Switch firstmate to OpenCode 2 and refresh nix-darwin pins
Point fm at the isolated oc2 wrapper, upgrade Homebrew packages on rebuild, and declare ffmpeg, docker-desktop, puremac, utm, node, and tea. Update nixpkgs, home-manager, and nix-homebrew locks. Track Pi agent config, crew-dispatch, and the Gitea PR helper that home.nix already expected.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"when": "pure-function implementation with tests (indicators, slots, seasonal wiring, repositories)",
|
||||
"use": [{ "harness": "pi", "model": "opencode-go/mimo-v2.5-pro" }],
|
||||
"why": "MiMo V2.5 Pro (opencode-go, 1M ctx) produces clean correct code; flat-rate sub so $0 marginal cost. Pi drives it in an isolated worktree."
|
||||
},
|
||||
{
|
||||
"when": "adapter or external-fetch integration (COT, seed backfills, vendor seams)",
|
||||
"use": [{ "harness": "pi", "model": "opencode-go/mimo-v2.5-pro" }],
|
||||
"why": "Pattern-following work against existing adapter contracts; MiMo handles iterative fetch/debug loops."
|
||||
},
|
||||
{
|
||||
"when": "frontend page or tRPC wiring (UI panels, router procedures, client exposure)",
|
||||
"use": [{ "harness": "pi", "model": "opencode-go/mimo-v2.5-pro" }],
|
||||
"why": "Mechanical wiring against existing components/routers; the contract carries the specialization."
|
||||
}
|
||||
],
|
||||
"default": [{ "harness": "pi", "model": "opencode-go/mimo-v2.5-pro" }]
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"model": "talos/ornith-worker-1",
|
||||
"autoupdate": false,
|
||||
"providers": {
|
||||
"talos": {
|
||||
"name": "Talos Cluster",
|
||||
"package": "@opencode-ai/ai/providers/openai-compatible",
|
||||
"settings": {
|
||||
"baseURL": "http://10.37.0.165:30081/v1"
|
||||
},
|
||||
"models": {
|
||||
"ornith-worker-1": {
|
||||
"modelID": "ornith-worker-1",
|
||||
"name": "Ornith Worker 1 (RTX 4070 Super)",
|
||||
"capabilities": {
|
||||
"tools": true,
|
||||
"input": ["text"],
|
||||
"output": ["text"]
|
||||
},
|
||||
"limit": {
|
||||
"context": 132768,
|
||||
"output": 65536
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"talos-worker-2": {
|
||||
"name": "Talos Worker 2",
|
||||
"package": "@opencode-ai/ai/providers/openai-compatible",
|
||||
"settings": {
|
||||
"baseURL": "http://10.37.0.219:30082/v1"
|
||||
},
|
||||
"models": {
|
||||
"genesis-hermes-worker-2": {
|
||||
"modelID": "/models/Hermes3.6-35B-A3B-Uncensored-Genesis-V6-APEX.gguf",
|
||||
"name": "Genesis Hermes V6 APEX (RTX 3080 12GB, vision)",
|
||||
"capabilities": {
|
||||
"tools": true,
|
||||
"input": ["text"],
|
||||
"output": ["text"]
|
||||
},
|
||||
"limit": {
|
||||
"context": 131072,
|
||||
"output": 65536
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"omlx": {
|
||||
"name": "oMLX (Local)",
|
||||
"package": "@opencode-ai/ai/providers/openai-compatible",
|
||||
"settings": {
|
||||
"baseURL": "http://localhost:8000/v1"
|
||||
},
|
||||
"models": {
|
||||
"ornith-35b": {
|
||||
"modelID": "Ornith-35B",
|
||||
"name": "Ornith 35B (Qwen3.5 MoE, 4-bit, 131K ctx)",
|
||||
"capabilities": {
|
||||
"tools": true,
|
||||
"input": ["text"],
|
||||
"output": ["text"]
|
||||
},
|
||||
"limit": {
|
||||
"context": 131072,
|
||||
"output": 65536
|
||||
}
|
||||
},
|
||||
"ornith-5bit": {
|
||||
"modelID": "Ornith-1.0-35B-5bit-mlx",
|
||||
"name": "Ornith 35B (5-bit, 262K ctx)",
|
||||
"capabilities": {
|
||||
"tools": true,
|
||||
"input": ["text"],
|
||||
"output": ["text"]
|
||||
},
|
||||
"limit": {
|
||||
"context": 262144,
|
||||
"output": 65536
|
||||
}
|
||||
},
|
||||
"ternary-bonsai": {
|
||||
"modelID": "Ternary-Bonsai-27B-mlx-2bit",
|
||||
"name": "Ternary Bonsai 27B (2-bit, ternary weights)",
|
||||
"capabilities": {
|
||||
"tools": true,
|
||||
"input": ["text"],
|
||||
"output": ["text"]
|
||||
},
|
||||
"limit": {
|
||||
"context": 131072,
|
||||
"output": 65536
|
||||
}
|
||||
},
|
||||
"dagger-qwen36-27b": {
|
||||
"modelID": "Dagger-Qwen3.6-27B-MLX",
|
||||
"name": "Dagger Qwen3.6 27B MLX (M5, 32GB)",
|
||||
"capabilities": {
|
||||
"tools": true,
|
||||
"input": ["text"],
|
||||
"output": ["text"]
|
||||
},
|
||||
"limit": {
|
||||
"context": 65536,
|
||||
"output": 65536
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Kun Chen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,157 @@
|
||||
// Pi Calm - a standalone conversation-presentation toggle for Pi.
|
||||
//
|
||||
// Adapted from the Firstmate project's Calm implementation.
|
||||
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
|
||||
//
|
||||
// Verified against Pi 0.82.0, which exports its shared tool-row component,
|
||||
// session_start replacement reasons, agent_start
|
||||
// and agent_settled, ExtensionUIContext.setToolsExpanded(), setWorkingVisible(),
|
||||
// setWidget() with a disposable component factory, and setHiddenThinkingLabel().
|
||||
// ./lib/working-ship.ts owns the animated working presentation this file
|
||||
// installs. ./lib/preference.ts owns the local state file. The collapsed-thinking
|
||||
// presentation adapter probes the exact public API seam it patches and degrades
|
||||
// independently with one clear diagnostic (see installCalmPresentationAdapter
|
||||
// below) if a future Pi removes it. The shared tool-row adapter is limited to
|
||||
// Pi's seven known built-in names, so generic custom tools and unsupported
|
||||
// transcript classes deliberately stay visible.
|
||||
//
|
||||
// Calm changes presentation only. It never intercepts, transforms, reroutes,
|
||||
// removes, or reorders semantic input, tool execution, model context, session
|
||||
// storage, or export data; /export and /share render the complete stock
|
||||
// transcript.
|
||||
import { type ExtensionAPI, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
||||
import { getKeybindings } from "@earendil-works/pi-tui";
|
||||
import { installCalmBuiltInToolShellLayout } from "./lib/built-in-tool-shells.ts";
|
||||
import { installCalmCollapsedThinkingLayout } from "./lib/collapsed-thinking.ts";
|
||||
import { loadCalmPreference, persistCalmPreference } from "./lib/preference.ts";
|
||||
import {
|
||||
calmPresentationIsActive,
|
||||
setCalmPresentation,
|
||||
setCalmStockExportRendering,
|
||||
} from "./lib/visibility.ts";
|
||||
import {
|
||||
CALM_WORKING_SHIP_WIDGET_KEY,
|
||||
createCalmWorkingShipAnimation,
|
||||
createCalmWorkingShipWidget,
|
||||
} from "./lib/working-ship.ts";
|
||||
|
||||
// Each presentation adapter probes the exact Pi API it patches. If a future Pi
|
||||
// removes that API, only the affected adapter degrades; the rest of Calm keeps
|
||||
// working.
|
||||
function installCalmPresentationAdapter(name: string, install: () => void): void {
|
||||
try {
|
||||
install();
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Pi Calm: ${name} presentation adapter unavailable, skipping. ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
installCalmPresentationAdapter("collapsed-thinking", installCalmCollapsedThinkingLayout);
|
||||
installCalmPresentationAdapter("built-in-tool-shells", installCalmBuiltInToolShellLayout);
|
||||
|
||||
let removeTerminalInputHandler: (() => void) | undefined;
|
||||
// One logical agent run, tracked from agent_start through agent_settled rather
|
||||
// than from turns or tool calls, so the boat never flickers between tool calls,
|
||||
// automatic continuations, retries, or compaction that stay inside the same run.
|
||||
let agentRunActive = false;
|
||||
let workingShipShown = false;
|
||||
// One animation instance per extension lifetime. Hiding the working widget
|
||||
// freezes this state; the next working period resumes it. session_start resets
|
||||
// it so a fresh Pi session starts at the normal initial position. Never
|
||||
// module-global.
|
||||
const workingShipAnimation = createCalmWorkingShipAnimation();
|
||||
|
||||
// Single owner of Calm's working-row presentation choice. The widget is only
|
||||
// created or removed on a real transition, so repeated starts cannot duplicate
|
||||
// its timer.
|
||||
const applyWorkingPresentation = (
|
||||
ui: ExtensionUIContext,
|
||||
forceStockVisibility = false,
|
||||
): void => {
|
||||
const showShip = agentRunActive && calmPresentationIsActive();
|
||||
if (showShip !== workingShipShown) {
|
||||
workingShipShown = showShip;
|
||||
ui.setWidget(
|
||||
CALM_WORKING_SHIP_WIDGET_KEY,
|
||||
showShip
|
||||
? (tui) => createCalmWorkingShipWidget(tui, workingShipAnimation)
|
||||
: undefined,
|
||||
);
|
||||
ui.setWorkingVisible(!showShip);
|
||||
} else if (forceStockVisibility && !showShip) {
|
||||
ui.setWorkingVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
pi.on("session_start", (_event, ctx) => {
|
||||
setCalmPresentation(loadCalmPreference());
|
||||
setCalmStockExportRendering(false);
|
||||
agentRunActive = false;
|
||||
workingShipShown = false;
|
||||
// A genuine new session lifetime starts the boat at the normal initial position.
|
||||
workingShipAnimation.reset();
|
||||
applyWorkingPresentation(ctx.ui, true);
|
||||
ctx.ui.setHiddenThinkingLabel(calmPresentationIsActive() ? "" : undefined);
|
||||
removeTerminalInputHandler?.();
|
||||
removeTerminalInputHandler = ctx.ui.onTerminalInput((data) => {
|
||||
if (!getKeybindings().matches(data, "tui.input.submit")) return;
|
||||
|
||||
const input = ctx.ui.getEditorText().trim();
|
||||
if (
|
||||
input !== "/share" &&
|
||||
input !== "/export" &&
|
||||
!input.startsWith("/export ")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// /export and /share render through the same tool renderers the transcript
|
||||
// uses, so force stock output for the duration of the command. Session and
|
||||
// export data are never filtered; this only concerns the visual components.
|
||||
setCalmStockExportRendering(true);
|
||||
setTimeout(() => {
|
||||
setCalmStockExportRendering(false);
|
||||
const expanded = ctx.ui.getToolsExpanded();
|
||||
ctx.ui.setToolsExpanded(!expanded);
|
||||
ctx.ui.setToolsExpanded(expanded);
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
pi.on("agent_start", (_event, ctx) => {
|
||||
agentRunActive = true;
|
||||
applyWorkingPresentation(ctx.ui);
|
||||
});
|
||||
|
||||
// agent_settled is emitted from a finally block, so it also covers abort and failure.
|
||||
pi.on("agent_settled", (_event, ctx) => {
|
||||
agentRunActive = false;
|
||||
applyWorkingPresentation(ctx.ui);
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", (_event, ctx) => {
|
||||
agentRunActive = false;
|
||||
applyWorkingPresentation(ctx.ui);
|
||||
});
|
||||
|
||||
pi.registerCommand("calm", {
|
||||
description: "Toggle Calm: hide collapsed thinking and built-in tool shells from the transcript (presentation only).",
|
||||
handler: async (_args, ctx) => {
|
||||
const active = !calmPresentationIsActive();
|
||||
// Persist first: if the state file cannot be written, the toggle fails
|
||||
// with a clear error instead of silently reverting on the next restart.
|
||||
persistCalmPreference(active);
|
||||
setCalmPresentation(active);
|
||||
applyWorkingPresentation(ctx.ui, true);
|
||||
ctx.ui.setHiddenThinkingLabel(active ? "" : undefined);
|
||||
|
||||
// Flip expansion twice to force a transcript redraw while preserving the
|
||||
// user's exact Ctrl+O tools-expanded state.
|
||||
const expanded = ctx.ui.getToolsExpanded();
|
||||
ctx.ui.setToolsExpanded(!expanded);
|
||||
ctx.ui.setToolsExpanded(expanded);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Pi Calm - gapless built-in tool-shell presentation adapter.
|
||||
//
|
||||
// Adapted from the Firstmate project's Calm implementation.
|
||||
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
|
||||
//
|
||||
// Verified against Pi 0.82.0, which exports AgentSession and
|
||||
// ToolExecutionComponent. The source-aware lookup returns Pi's active definition
|
||||
// unchanged and the adapter changes only its final TUI row layout. Execution,
|
||||
// settings, SDK overrides, extension collisions, and stored results remain
|
||||
// owned by Pi. Image results remain visible without their call/result shell,
|
||||
// and custom tools or tools outside Pi's seven built-ins render unchanged.
|
||||
import {
|
||||
AgentSession,
|
||||
ToolExecutionComponent,
|
||||
type ToolDefinition,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import type { Component } from "@earendil-works/pi-tui";
|
||||
import { calmHidesTranscriptChrome } from "./visibility.ts";
|
||||
|
||||
const CALM_BUILT_IN_TOOL_NAMES = new Set([
|
||||
"read",
|
||||
"bash",
|
||||
"edit",
|
||||
"write",
|
||||
"grep",
|
||||
"find",
|
||||
"ls",
|
||||
]);
|
||||
|
||||
type ToolRowPresentationState = {
|
||||
toolName: string;
|
||||
toolDefinition?: ToolDefinition;
|
||||
imageComponents: Component[];
|
||||
imageSpacers: Component[];
|
||||
};
|
||||
|
||||
type AgentSessionPresentationState = {
|
||||
_baseToolsOverride?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type CalmBuiltInToolShellPatch = {
|
||||
hidesShell: () => boolean;
|
||||
builtInDefinitions: WeakSet<ToolDefinition>;
|
||||
};
|
||||
|
||||
const CALM_BUILT_IN_TOOL_SHELL_PATCH = Symbol.for(
|
||||
"pi-calm:built-in-tool-shell-layout:pi-0.82.0",
|
||||
);
|
||||
|
||||
export function installCalmBuiltInToolShellLayout(): void {
|
||||
const registry = globalThis as typeof globalThis & {
|
||||
[key: symbol]: CalmBuiltInToolShellPatch | undefined;
|
||||
};
|
||||
const hidesShell = (): boolean => calmHidesTranscriptChrome();
|
||||
const installed = registry[CALM_BUILT_IN_TOOL_SHELL_PATCH];
|
||||
if (installed?.builtInDefinitions) {
|
||||
installed.hidesShell = hidesShell;
|
||||
return;
|
||||
}
|
||||
|
||||
const originalGetToolDefinition = AgentSession.prototype.getToolDefinition;
|
||||
if (typeof originalGetToolDefinition !== "function") {
|
||||
throw new Error("Pi Calm requires Pi AgentSession.getToolDefinition");
|
||||
}
|
||||
if (typeof ToolExecutionComponent !== "function") {
|
||||
throw new Error("Pi Calm requires Pi ToolExecutionComponent");
|
||||
}
|
||||
const originalRender = ToolExecutionComponent.prototype.render;
|
||||
if (typeof originalRender !== "function") {
|
||||
throw new Error("Pi Calm requires Pi ToolExecutionComponent.render");
|
||||
}
|
||||
|
||||
if (installed) installed.hidesShell = () => false;
|
||||
|
||||
const patch: CalmBuiltInToolShellPatch = {
|
||||
hidesShell,
|
||||
builtInDefinitions: new WeakSet(),
|
||||
};
|
||||
AgentSession.prototype.getToolDefinition = function (
|
||||
name: string,
|
||||
): ToolDefinition | undefined {
|
||||
const definition = originalGetToolDefinition.call(this, name);
|
||||
const source = this.getAllTools().find((tool) => tool.name === name)?.sourceInfo.source;
|
||||
if (definition) {
|
||||
const session = this as unknown as AgentSessionPresentationState;
|
||||
const isSdkBaseOverride = Object.hasOwn(session._baseToolsOverride ?? {}, name);
|
||||
if (source === "builtin" && !isSdkBaseOverride) {
|
||||
patch.builtInDefinitions.add(definition);
|
||||
} else {
|
||||
patch.builtInDefinitions.delete(definition);
|
||||
}
|
||||
}
|
||||
return definition;
|
||||
};
|
||||
|
||||
ToolExecutionComponent.prototype.render = function (width: number): string[] {
|
||||
const state = this as unknown as ToolRowPresentationState;
|
||||
const isKnownBuiltIn =
|
||||
CALM_BUILT_IN_TOOL_NAMES.has(state.toolName) &&
|
||||
state.toolDefinition !== undefined &&
|
||||
patch.builtInDefinitions.has(state.toolDefinition);
|
||||
if (!isKnownBuiltIn || !patch.hidesShell()) {
|
||||
return originalRender.call(this, width);
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
for (let index = 0; index < state.imageComponents.length; index += 1) {
|
||||
const spacer = state.imageSpacers[index];
|
||||
if (spacer) lines.push(...spacer.render(width));
|
||||
const image = state.imageComponents[index];
|
||||
if (image) lines.push(...image.render(width));
|
||||
}
|
||||
return lines;
|
||||
};
|
||||
|
||||
registry[CALM_BUILT_IN_TOOL_SHELL_PATCH] = patch;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Pi Calm - gapless collapsed-thinking presentation adapter.
|
||||
//
|
||||
// Adapted from the Firstmate project's Calm implementation.
|
||||
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
|
||||
//
|
||||
// Verified against Pi 0.82.0, which exports AssistantMessageComponent with an
|
||||
// updateContent method. installCalmCollapsedThinkingLayout() probes that exact
|
||||
// public seam and throws if it is missing; index.ts catches that and skips only
|
||||
// this adapter with one clear diagnostic instead of blocking Calm or Pi.
|
||||
//
|
||||
// How it works: Pi renders a hidden thinking block as one static label row.
|
||||
// Calm sets that label to the empty string and this adapter filters thinking
|
||||
// blocks out of the message handed to the stock renderer, so a collapsed
|
||||
// thinking block occupies zero rows instead of one blank one. The unfiltered
|
||||
// message is kept on lastMessage so expanding thinking (Ctrl+T) and turning
|
||||
// Calm off both restore the original reasoning content byte-for-byte. Only
|
||||
// collapsed thinking is affected: expanded reasoning, assistant text, and tool
|
||||
// calls render exactly as Pi renders them.
|
||||
import type { AssistantMessageComponent as PiAssistantMessageComponent } from "@earendil-works/pi-coding-agent";
|
||||
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
|
||||
import { calmHidesTranscriptChrome } from "./visibility.ts";
|
||||
|
||||
type AssistantMessage = Parameters<PiAssistantMessageComponent["updateContent"]>[0];
|
||||
|
||||
type AssistantMessagePresentationState = {
|
||||
hiddenThinkingLabel: string;
|
||||
hideThinkingBlock: boolean;
|
||||
lastMessage?: AssistantMessage;
|
||||
};
|
||||
|
||||
type CalmCollapsedThinkingPatch = {
|
||||
hidesThinking: () => boolean;
|
||||
};
|
||||
|
||||
// Keep the introduction-version symbol stable so a compatible upgrade cannot
|
||||
// double-patch a live process.
|
||||
const CALM_COLLAPSED_THINKING_PATCH = Symbol.for(
|
||||
"pi-calm:collapsed-thinking-layout:pi-0.82.0",
|
||||
);
|
||||
|
||||
export function installCalmCollapsedThinkingLayout(): void {
|
||||
const registry = globalThis as typeof globalThis & {
|
||||
[key: symbol]: CalmCollapsedThinkingPatch | undefined;
|
||||
};
|
||||
const hidesThinking = (): boolean => calmHidesTranscriptChrome();
|
||||
const installed = registry[CALM_COLLAPSED_THINKING_PATCH];
|
||||
if (installed) {
|
||||
installed.hidesThinking = hidesThinking;
|
||||
return;
|
||||
}
|
||||
|
||||
const patch: CalmCollapsedThinkingPatch = { hidesThinking };
|
||||
const AssistantMessageComponent = PiCodingAgent.AssistantMessageComponent;
|
||||
if (typeof AssistantMessageComponent !== "function") {
|
||||
throw new Error("Pi Calm requires Pi AssistantMessageComponent");
|
||||
}
|
||||
const originalUpdateContent = AssistantMessageComponent.prototype.updateContent;
|
||||
if (typeof originalUpdateContent !== "function") {
|
||||
throw new Error("Pi Calm requires Pi AssistantMessageComponent.updateContent");
|
||||
}
|
||||
|
||||
AssistantMessageComponent.prototype.updateContent = function (
|
||||
message: AssistantMessage,
|
||||
): void {
|
||||
const state = this as unknown as AssistantMessagePresentationState;
|
||||
const hideThinking =
|
||||
state.hiddenThinkingLabel === "" &&
|
||||
state.hideThinkingBlock &&
|
||||
patch.hidesThinking();
|
||||
const presentationMessage = hideThinking
|
||||
? {
|
||||
...message,
|
||||
content: message.content.filter((block) => block.type !== "thinking"),
|
||||
}
|
||||
: message;
|
||||
|
||||
originalUpdateContent.call(this, presentationMessage);
|
||||
if (presentationMessage !== message) state.lastMessage = message;
|
||||
};
|
||||
|
||||
registry[CALM_COLLAPSED_THINKING_PATCH] = patch;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Pi Calm - persisted on/off preference.
|
||||
//
|
||||
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
|
||||
//
|
||||
// The preference lives in a plain local state file named "calm" directly under
|
||||
// Pi's agent directory (~/.pi/agent by default, PI_CODING_AGENT_DIR when set).
|
||||
// That directory is Pi runtime territory: this repository never tracks the
|
||||
// state file and Home Manager never manages it. The file contains exactly
|
||||
// "on\n" or "off\n"; anything else, including a missing or unreadable file,
|
||||
// means off.
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export const CALM_PREFERENCE_FILE_NAME = "calm";
|
||||
|
||||
/**
|
||||
* Resolve Pi's agent directory through Pi's exported getAgentDir(), which
|
||||
* honors PI_CODING_AGENT_DIR and tilde expansion. If a future Pi stops
|
||||
* exporting it, fall back to the documented environment variable and default
|
||||
* path instead of failing.
|
||||
*/
|
||||
export function calmAgentDir(): string {
|
||||
if (typeof PiCodingAgent.getAgentDir === "function") return PiCodingAgent.getAgentDir();
|
||||
const envDir = process.env.PI_CODING_AGENT_DIR?.trim();
|
||||
if (envDir) return envDir;
|
||||
return join(homedir(), ".pi", "agent");
|
||||
}
|
||||
|
||||
export function calmPreferencePath(): string {
|
||||
return join(calmAgentDir(), CALM_PREFERENCE_FILE_NAME);
|
||||
}
|
||||
|
||||
/** Load the persisted preference. Calm is off by default and on any read error. */
|
||||
export function loadCalmPreference(): boolean {
|
||||
try {
|
||||
return readFileSync(calmPreferencePath(), "utf8").trim() === "on";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the preference atomically (unique temp file plus rename) so a
|
||||
* crashed write never leaves a truncated state file. A failure throws a clear
|
||||
* error naming the path so /calm can surface it instead of silently applying
|
||||
* a toggle that would not survive a restart.
|
||||
*/
|
||||
export function persistCalmPreference(active: boolean): void {
|
||||
const path = calmPreferencePath();
|
||||
try {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
||||
try {
|
||||
writeFileSync(temporaryPath, active ? "on\n" : "off\n", {
|
||||
encoding: "utf8",
|
||||
flag: "wx",
|
||||
mode: 0o600,
|
||||
});
|
||||
renameSync(temporaryPath, path);
|
||||
} finally {
|
||||
rmSync(temporaryPath, { force: true });
|
||||
}
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Pi Calm could not persist its preference to ${path}: ${reason}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Pi Calm - shared presentation state for the standalone Calm extension.
|
||||
//
|
||||
// Adapted from the Firstmate project's Calm implementation.
|
||||
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
|
||||
//
|
||||
// This module owns only the in-memory presentation flags. Presentation filtering
|
||||
// must never delete or alter semantic, session, or export data, so the export
|
||||
// path forces stock rendering for the duration of an /export or /share command.
|
||||
|
||||
let active = false;
|
||||
let stockExportRendering = false;
|
||||
|
||||
/** True while Calm presentation filtering is enabled. */
|
||||
export function calmPresentationIsActive(): boolean {
|
||||
return active;
|
||||
}
|
||||
|
||||
export function setCalmPresentation(next: boolean): void {
|
||||
active = next;
|
||||
}
|
||||
|
||||
/** True while an /export or /share render is in flight and stock output is required. */
|
||||
export function calmStockExportRenderingIsActive(): boolean {
|
||||
return stockExportRendering;
|
||||
}
|
||||
|
||||
export function setCalmStockExportRendering(next: boolean): void {
|
||||
stockExportRendering = next;
|
||||
}
|
||||
|
||||
/**
|
||||
* True while Calm should hide the supported transcript chrome: collapsed
|
||||
* thinking labels and the known Pi built-in tool call/result shells. Genuine
|
||||
* user prompts, assistant text, custom tools, and every other transcript row
|
||||
* class are never filtered by this flag.
|
||||
*/
|
||||
export function calmHidesTranscriptChrome(): boolean {
|
||||
return active && !stockExportRendering;
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
// Pi Calm - animated working presentation.
|
||||
//
|
||||
// Adapted from the Firstmate project's Calm implementation.
|
||||
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
|
||||
//
|
||||
// Calm replaces Pi's stock working row with a tiny two-row ASCII boat while one
|
||||
// logical agent run is active. This module owns only the sprite geometry, the
|
||||
// bounce track, the two animation cadences, the session-scoped freeze/resume
|
||||
// state, and the temporary TUI widget; ../index.ts owns when the presentation
|
||||
// is installed and removed, and stays the sole caller of setWorkingVisible().
|
||||
//
|
||||
// Cadence: one scheduler drives two logically independent clocks. Every tick
|
||||
// advances the water phase, and only every CALM_WORKING_SHIP_TICKS_PER_MOVE-th
|
||||
// tick moves the boat, so the water visibly ripples several times between boat
|
||||
// steps and the boat itself reads as calm. Both clocks stop together when the
|
||||
// widget is disposed. Ticks, not wall-clock timestamps, drive every state
|
||||
// change, so tests can seek time exactly.
|
||||
//
|
||||
// Continuity: one extension-owned animation instance survives hide/show within
|
||||
// the same Pi process and Calm extension lifetime. Disposing the widget freezes
|
||||
// column, direction, water phase, and tick cadence without advancing them for
|
||||
// hidden wall time. The next working period resumes from that exact logical
|
||||
// state. A fresh session or new extension lifetime calls reset() and starts at
|
||||
// the normal initial position. State is never a module-level or process-global
|
||||
// singleton.
|
||||
//
|
||||
// Verified against Pi 0.82.0, which exposes ExtensionUIContext.setWidget() with
|
||||
// a component factory, per-widget dispose(), and TUI.requestRender(). Pi renders
|
||||
// a widget through Component.render(width), so this module recomputes its track
|
||||
// from that width on every frame instead of caching a terminal size that a
|
||||
// resize would invalidate. A resize while the boat is hidden is applied on the
|
||||
// first resumed frame through the same clamp path.
|
||||
import type { Component, TUI } from "@earendil-works/pi-tui";
|
||||
|
||||
// The hull is symmetric and replaces waves on its row rather than adding a third row.
|
||||
const HULL = "\\__/";
|
||||
// A mainsail extends aft of the mast, so it trails behind the bow relative to travel.
|
||||
const SAIL_RIGHT = "<|";
|
||||
const SAIL_LEFT = "|>";
|
||||
// Centers the two-cell sail over the four-cell hull.
|
||||
const SAIL_OFFSET = 1;
|
||||
const HULL_WIDTH = HULL.length;
|
||||
const SAIL_WIDTH = SAIL_RIGHT.length;
|
||||
|
||||
// Bounded deterministic fixed-cell water phases. Every entry is exactly one column, so
|
||||
// advancing the phase ripples the surface without changing visible width or row count.
|
||||
const WAVE_CYCLE = ["~", "~", "-", "~"] as const;
|
||||
|
||||
// Standard ANSI foreground codes only: no theme lookup, bright variant, or 256/RGB.
|
||||
const BLUE = "\u001b[34m";
|
||||
const YELLOW = "\u001b[33m";
|
||||
// Restores the default foreground so color never bleeds into padding or later frames.
|
||||
const RESET = "\u001b[39m";
|
||||
|
||||
export const CALM_WORKING_SHIP_WIDGET_KEY = "calm-working-ship";
|
||||
/** Scheduler period. One tick advances the water by one phase. */
|
||||
export const CALM_WORKING_SHIP_TICK_MS = 220;
|
||||
/** Boat moves one column every Nth tick, so it travels at 220 * 4 = 880ms per column. */
|
||||
export const CALM_WORKING_SHIP_TICKS_PER_MOVE = 4;
|
||||
|
||||
export type CalmWorkingShipAnimation = {
|
||||
/** Render one frame that exactly fits `width`, clamping the track to it first. */
|
||||
render(width: number): string[];
|
||||
/** Advance one scheduler tick: water every tick, boat on its slower cadence. */
|
||||
tick(): void;
|
||||
restoreLastRendered(): void;
|
||||
/** Restore the normal initial column, direction, water phase, and cadence. */
|
||||
reset(): void;
|
||||
/**
|
||||
* Clamp the frozen column and direction to `width` without advancing time.
|
||||
* Used when a terminal resize lands while the working presentation is hidden.
|
||||
*/
|
||||
clampToWidth(width: number): void;
|
||||
/** Current hull column, exposed for deterministic motion assertions. */
|
||||
position(): number;
|
||||
/** Current travel direction: 1 travelling right, -1 travelling left. */
|
||||
direction(): number;
|
||||
/** Current water phase, exposed for deterministic ripple assertions. */
|
||||
waterPhase(): number;
|
||||
};
|
||||
|
||||
/** Longest hull start column that still fits the sprite in `width` usable cells. */
|
||||
function trackSpan(width: number): number {
|
||||
if (width >= HULL_WIDTH) return width - HULL_WIDTH;
|
||||
if (width >= SAIL_WIDTH) return width - SAIL_WIDTH;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function createCalmWorkingShipAnimation(): CalmWorkingShipAnimation {
|
||||
let position = 0;
|
||||
let direction = 1;
|
||||
let span = 0;
|
||||
let phase = 0;
|
||||
let ticks = 0;
|
||||
let renderedPosition = position;
|
||||
let renderedDirection = direction;
|
||||
let renderedSpan = span;
|
||||
let renderedPhase = phase;
|
||||
let renderedTicks = ticks;
|
||||
|
||||
// Reversing the moment the boat lands on an endpoint means the endpoint frame itself
|
||||
// already shows the new heading, so no frame at or after a bounce shows the old sail.
|
||||
const settleDirectionAtEdges = (): void => {
|
||||
if (span <= 0) return;
|
||||
if (position >= span) direction = -1;
|
||||
else if (position <= 0) direction = 1;
|
||||
};
|
||||
|
||||
const applyWidth = (width: number): void => {
|
||||
if (width <= 0) {
|
||||
span = 0;
|
||||
position = 0;
|
||||
return;
|
||||
}
|
||||
span = trackSpan(width);
|
||||
position = Math.min(position, span);
|
||||
settleDirectionAtEdges();
|
||||
};
|
||||
|
||||
const commitRenderedState = (): void => {
|
||||
renderedPosition = position;
|
||||
renderedDirection = direction;
|
||||
renderedSpan = span;
|
||||
renderedPhase = phase;
|
||||
renderedTicks = ticks;
|
||||
};
|
||||
|
||||
const restoreLastRenderedState = (): void => {
|
||||
position = renderedPosition;
|
||||
direction = renderedDirection;
|
||||
span = renderedSpan;
|
||||
phase = renderedPhase;
|
||||
ticks = renderedTicks;
|
||||
};
|
||||
|
||||
/** One colored run of water covering absolute columns [from, from + count). */
|
||||
const water = (from: number, count: number): string => {
|
||||
if (count <= 0) return "";
|
||||
let cells = "";
|
||||
for (let column = from; column < from + count; column += 1) {
|
||||
cells += WAVE_CYCLE[(column + phase) % WAVE_CYCLE.length];
|
||||
}
|
||||
return `${BLUE}${cells}${RESET}`;
|
||||
};
|
||||
|
||||
const boat = (text: string): string => `${YELLOW}${text}${RESET}`;
|
||||
|
||||
return {
|
||||
position: () => position,
|
||||
direction: () => direction,
|
||||
waterPhase: () => phase,
|
||||
|
||||
restoreLastRendered: restoreLastRenderedState,
|
||||
|
||||
reset(): void {
|
||||
position = 0;
|
||||
direction = 1;
|
||||
span = 0;
|
||||
phase = 0;
|
||||
ticks = 0;
|
||||
commitRenderedState();
|
||||
},
|
||||
|
||||
clampToWidth(width: number): void {
|
||||
applyWidth(width);
|
||||
},
|
||||
|
||||
tick(): void {
|
||||
ticks += 1;
|
||||
phase = (phase + 1) % WAVE_CYCLE.length;
|
||||
if (ticks % CALM_WORKING_SHIP_TICKS_PER_MOVE !== 0) return;
|
||||
if (span <= 0) {
|
||||
position = 0;
|
||||
return;
|
||||
}
|
||||
position = Math.min(span, Math.max(0, position + direction));
|
||||
settleDirectionAtEdges();
|
||||
},
|
||||
|
||||
render(width: number): string[] {
|
||||
if (width <= 0) return [];
|
||||
|
||||
// A resize lands here before the next frame, so recompute and clamp the track
|
||||
// immediately rather than trusting a position measured against the old width.
|
||||
applyWidth(width);
|
||||
|
||||
const sail = direction >= 0 ? SAIL_RIGHT : SAIL_LEFT;
|
||||
|
||||
let frame: string[];
|
||||
if (width < SAIL_WIDTH) {
|
||||
// Too narrow for even the sail: a deterministic single row of water.
|
||||
frame = [water(0, width)];
|
||||
} else if (width < HULL_WIDTH) {
|
||||
// Too narrow for the hull: the sail alone rides the water row.
|
||||
frame = [
|
||||
water(0, position) +
|
||||
boat(sail) +
|
||||
water(position + SAIL_WIDTH, width - position - SAIL_WIDTH),
|
||||
];
|
||||
} else {
|
||||
frame = [
|
||||
" ".repeat(position + SAIL_OFFSET) + boat(sail),
|
||||
water(0, position) +
|
||||
boat(HULL) +
|
||||
water(position + HULL_WIDTH, width - position - HULL_WIDTH),
|
||||
];
|
||||
}
|
||||
|
||||
commitRenderedState();
|
||||
return frame;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the temporary Calm working widget bound to one caller-owned animation.
|
||||
* Pi disposes the previous component before installing a replacement under the same
|
||||
* key and when it clears extension widgets, so the single scheduler driving both
|
||||
* cadences cannot outlive the widget or duplicate. Disposing freezes the shared
|
||||
* animation in place; the next widget bound to the same animation resumes without
|
||||
* applying hidden wall time.
|
||||
*/
|
||||
export function createCalmWorkingShipWidget(
|
||||
tui: TUI,
|
||||
animation: CalmWorkingShipAnimation = createCalmWorkingShipAnimation(),
|
||||
): Component & { dispose(): void } {
|
||||
let disposed = false;
|
||||
const timer = setInterval(() => {
|
||||
if (disposed) return;
|
||||
animation.tick();
|
||||
tui.requestRender();
|
||||
}, CALM_WORKING_SHIP_TICK_MS);
|
||||
// The animation must never keep Pi's process alive on its own.
|
||||
timer.unref?.();
|
||||
|
||||
return {
|
||||
render: (width) => (disposed ? [] : animation.render(width)),
|
||||
// Every frame is rebuilt from fixed standard ANSI codes, so there is no cache.
|
||||
invalidate: () => {},
|
||||
dispose: () => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
clearInterval(timer);
|
||||
animation.restoreLastRendered();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
const DEFAULT_TITLE = "π";
|
||||
const PREFIX = "π";
|
||||
const MAX_TITLE_LENGTH = 40;
|
||||
const SPINNER_INTERVAL_MS = 120;
|
||||
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
|
||||
function truncateTitle(title) {
|
||||
if (title.length <= MAX_TITLE_LENGTH) return title;
|
||||
return title.slice(0, MAX_TITLE_LENGTH - 3) + "...";
|
||||
}
|
||||
|
||||
function basename(path) {
|
||||
if (!path) return DEFAULT_TITLE;
|
||||
|
||||
const trimmed = path.replace(/[\\/]+$/, "");
|
||||
if (!trimmed) return DEFAULT_TITLE;
|
||||
|
||||
return trimmed.split(/[\\/]/).pop() || DEFAULT_TITLE;
|
||||
}
|
||||
|
||||
function getSessionName(pi) {
|
||||
const name = pi.getSessionName?.();
|
||||
return typeof name === "string" ? name.trim() : "";
|
||||
}
|
||||
|
||||
function getRawTitle(pi, ctx) {
|
||||
return getSessionName(pi) || basename(ctx.cwd);
|
||||
}
|
||||
|
||||
function isSpinningStatus(status) {
|
||||
return status === "working";
|
||||
}
|
||||
|
||||
function statusIndicator(status, spinnerFrame) {
|
||||
if (isSpinningStatus(status)) {
|
||||
if (SPINNER_FRAMES.length === 0) return "◉";
|
||||
return SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
|
||||
}
|
||||
|
||||
if (status === "done") return "✓";
|
||||
if (status === "error") return "✗";
|
||||
return "○";
|
||||
}
|
||||
|
||||
function formatTitle(pi, ctx, status, spinnerFrame) {
|
||||
const rawTitle = getRawTitle(pi, ctx);
|
||||
const suffix = rawTitle === DEFAULT_TITLE ? DEFAULT_TITLE : `${PREFIX} | ${truncateTitle(rawTitle)}`;
|
||||
|
||||
return `${statusIndicator(status, spinnerFrame)} | ${suffix}`;
|
||||
}
|
||||
|
||||
export default function terminalStatusTitle(pi) {
|
||||
let status = "idle";
|
||||
let spinnerFrame = 0;
|
||||
let spinnerInterval;
|
||||
let deferredWrite;
|
||||
let lastCtx;
|
||||
|
||||
function clearDeferredWrite() {
|
||||
if (!deferredWrite) return;
|
||||
|
||||
clearTimeout(deferredWrite);
|
||||
deferredWrite = undefined;
|
||||
}
|
||||
|
||||
function writeTitle(ctx = lastCtx) {
|
||||
if (!ctx?.hasUI) return;
|
||||
|
||||
lastCtx = ctx;
|
||||
ctx.ui.setTitle(formatTitle(pi, ctx, status, spinnerFrame));
|
||||
}
|
||||
|
||||
function stopSpinner() {
|
||||
if (!spinnerInterval) return;
|
||||
|
||||
clearInterval(spinnerInterval);
|
||||
spinnerInterval = undefined;
|
||||
spinnerFrame = 0;
|
||||
}
|
||||
|
||||
function startSpinner(ctx) {
|
||||
if (!ctx?.hasUI || spinnerInterval) return;
|
||||
|
||||
spinnerFrame = 0;
|
||||
spinnerInterval = setInterval(() => {
|
||||
if (!isSpinningStatus(status)) {
|
||||
stopSpinner();
|
||||
return;
|
||||
}
|
||||
|
||||
spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
|
||||
writeTitle();
|
||||
}, SPINNER_INTERVAL_MS);
|
||||
spinnerInterval.unref?.();
|
||||
}
|
||||
|
||||
function setStatus(nextStatus, ctx) {
|
||||
clearDeferredWrite();
|
||||
status = nextStatus;
|
||||
lastCtx = ctx;
|
||||
|
||||
if (isSpinningStatus(status)) {
|
||||
startSpinner(ctx);
|
||||
} else {
|
||||
stopSpinner();
|
||||
}
|
||||
|
||||
writeTitle(ctx);
|
||||
}
|
||||
|
||||
function scheduleWrite(ctx) {
|
||||
clearDeferredWrite();
|
||||
deferredWrite = setTimeout(() => {
|
||||
deferredWrite = undefined;
|
||||
writeTitle(ctx);
|
||||
}, 0);
|
||||
deferredWrite.unref?.();
|
||||
}
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
setStatus("idle", ctx);
|
||||
scheduleWrite(ctx);
|
||||
});
|
||||
|
||||
pi.on("agent_start", async (_event, ctx) => {
|
||||
setStatus("working", ctx);
|
||||
});
|
||||
|
||||
pi.on("agent_settled", async (_event, ctx) => {
|
||||
setStatus("done", ctx);
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", async () => {
|
||||
clearDeferredWrite();
|
||||
stopSpinner();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"providers": {
|
||||
"genesis": {
|
||||
"baseUrl": "http://10.37.0.219:30082/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "none",
|
||||
"compat": {
|
||||
"maxTokensField": "max_tokens"
|
||||
},
|
||||
"models": [
|
||||
{
|
||||
"id": "genesis",
|
||||
"name": "Hermes3.6-35B-A3B-Uncensored-Genesis-V6-APEX (Local)",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"contextWindow": 132864,
|
||||
"maxTokens": 32768,
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ornith": {
|
||||
"baseUrl": "http://10.37.0.165:30081/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "none",
|
||||
"compat": {
|
||||
"maxTokensField": "max_tokens"
|
||||
},
|
||||
"models": [
|
||||
{
|
||||
"id": "ornith",
|
||||
"name": "Ornith 1.0 35B (Local)",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"contextWindow": 132864,
|
||||
"maxTokens": 32768,
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"omlx": {
|
||||
"baseUrl": "http://localhost:8000/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "none",
|
||||
"compat": {
|
||||
"maxTokensField": "max_tokens"
|
||||
},
|
||||
"models": [
|
||||
{
|
||||
"id": "Dagger-Qwen3.6-27B-MLX",
|
||||
"name": "Dagger Qwen3.6 27B MLX (M5, 32GB)",
|
||||
"reasoning": true,
|
||||
"input": ["text"],
|
||||
"contextWindow": 65536,
|
||||
"maxTokens": 32768,
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"openai-codex": {
|
||||
"modelOverrides": {
|
||||
"gpt-5.6-luna": {
|
||||
"contextWindow": 272000
|
||||
},
|
||||
"gpt-5.6-sol": {
|
||||
"contextWindow": 272000
|
||||
},
|
||||
"gpt-5.6-terra": {
|
||||
"contextWindow": 272000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"lastChangelogVersion": "0.84.1",
|
||||
"theme": "rose-pine-moon",
|
||||
"defaultProvider": "ornith",
|
||||
"defaultModel": "ornith",
|
||||
"models": [
|
||||
{
|
||||
"name": "Genesis (Local)",
|
||||
"provider": "genesis",
|
||||
"model": "/models/Hermes3.6-35B-A3B-Uncensored-Genesis-V6-APEX.gguf"
|
||||
},
|
||||
{
|
||||
"name": "Ornith 1.0 35B (Local)",
|
||||
"provider": "ornith",
|
||||
"model": "/models/ornith-1.0-35b-Q6_K.gguf"
|
||||
},
|
||||
{
|
||||
"name": "Dagger Qwen3.6 27B MLX (M5)",
|
||||
"provider": "omlx",
|
||||
"model": "Dagger-Qwen3.6-27B-MLX"
|
||||
}
|
||||
],
|
||||
"compaction": {
|
||||
"enabled": true,
|
||||
"reserveTokens": 16384,
|
||||
"keepRecentTokens": 8000
|
||||
},
|
||||
"packages": [
|
||||
"npm:pi-context",
|
||||
"npm:pi-web-access@0.14.0",
|
||||
"npm:@ryan_nookpi/pi-extension-codex-fast-mode@0.2.6",
|
||||
"git:github.com/algal/pi-openai-server-compaction@c6d593087709e9481223dc6c6c2269b371b5e055",
|
||||
"../../.automaton/plugins/pi-read-tweet"
|
||||
],
|
||||
"defaultThinkingLevel": "high",
|
||||
"images": {
|
||||
"blockImages": false
|
||||
},
|
||||
"terminal": {
|
||||
"showImages": false
|
||||
},
|
||||
"hideThinkingBlock": true,
|
||||
"quietStartup": true,
|
||||
"steeringMode": "all",
|
||||
"followUpMode": "all",
|
||||
"collapseChangelog": true
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "rose-pine-moon",
|
||||
"vars": {
|
||||
"base": "#232136",
|
||||
"surface": "#2a273f",
|
||||
"overlay": "#393552",
|
||||
"muted": "#6e6a86",
|
||||
"subtle": "#908caa",
|
||||
"text": "#e0def4",
|
||||
"love": "#eb6f92",
|
||||
"gold": "#f6c177",
|
||||
"rose": "#ea9a97",
|
||||
"pine": "#3e8fb0",
|
||||
"foam": "#9ccfd8",
|
||||
"iris": "#c4a7e7",
|
||||
"highlightLow": "#2a283e",
|
||||
"highlightMed": "#44415a",
|
||||
"highlightHigh": "#56526e"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "iris",
|
||||
"border": "overlay",
|
||||
"borderAccent": "iris",
|
||||
"borderMuted": "muted",
|
||||
"success": "foam",
|
||||
"error": "love",
|
||||
"warning": "gold",
|
||||
"muted": "subtle",
|
||||
"dim": "muted",
|
||||
"text": "text",
|
||||
"thinkingText": "subtle",
|
||||
"selectedBg": "highlightMed",
|
||||
"userMessageBg": "surface",
|
||||
"userMessageText": "text",
|
||||
"customMessageBg": "surface",
|
||||
"customMessageText": "text",
|
||||
"customMessageLabel": "iris",
|
||||
"toolPendingBg": "highlightLow",
|
||||
"toolSuccessBg": "surface",
|
||||
"toolErrorBg": "surface",
|
||||
"toolTitle": "foam",
|
||||
"toolOutput": "text",
|
||||
"mdHeading": "iris",
|
||||
"mdLink": "foam",
|
||||
"mdLinkUrl": "subtle",
|
||||
"mdCode": "rose",
|
||||
"mdCodeBlock": "text",
|
||||
"mdCodeBlockBorder": "overlay",
|
||||
"mdQuote": "subtle",
|
||||
"mdQuoteBorder": "foam",
|
||||
"mdHr": "overlay",
|
||||
"mdListBullet": "iris",
|
||||
"toolDiffAdded": "foam",
|
||||
"toolDiffRemoved": "love",
|
||||
"toolDiffContext": "subtle",
|
||||
"syntaxComment": "muted",
|
||||
"syntaxKeyword": "iris",
|
||||
"syntaxFunction": "foam",
|
||||
"syntaxVariable": "text",
|
||||
"syntaxString": "gold",
|
||||
"syntaxNumber": "rose",
|
||||
"syntaxType": "pine",
|
||||
"syntaxOperator": "iris",
|
||||
"syntaxPunctuation": "subtle",
|
||||
"thinkingOff": "muted",
|
||||
"thinkingMinimal": "pine",
|
||||
"thinkingLow": "foam",
|
||||
"thinkingMedium": "iris",
|
||||
"thinkingHigh": "rose",
|
||||
"thinkingXhigh": "love",
|
||||
"thinkingMax": "gold",
|
||||
"bashMode": "gold"
|
||||
}
|
||||
}
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
# fm-gitea-pr.sh - Push a reviewed crewmate branch to Gitea and open a PR via tea.
|
||||
#
|
||||
# Used after the first mate (opencode) reviews a crewmate's local branch in
|
||||
# local-only mode: this helper pushes the branch to the Gitea remote and opens
|
||||
# a pull request for the record. The review decision already happened in
|
||||
# opencode; this is the publish step.
|
||||
#
|
||||
# Usage:
|
||||
# fm-gitea-pr.sh <branch> [--title "<title>"] [--body "<body>"] [--base <base>]
|
||||
#
|
||||
# Defaults: --title = branch name, --body = "(reviewed by first mate)", --base = main
|
||||
#
|
||||
# Prerequisites (one-time per machine, not automatable in nix - secret):
|
||||
# tea login add --name unraid --host http://unraid.local:3003 --token <GITEA_TOKEN>
|
||||
#
|
||||
# The Gitea remote is read from the current git repo's `origin` URL.
|
||||
set -euo pipefail
|
||||
|
||||
BRANCH=""
|
||||
TITLE=""
|
||||
BODY="(reviewed by first mate - tests green, Primary-Rule + ADR-0009 lint passed)"
|
||||
BASE="main"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--title) TITLE="$2"; shift 2 ;;
|
||||
--body) BODY="$2"; shift 2 ;;
|
||||
--base) BASE="$2"; shift 2 ;;
|
||||
-*) echo "unknown flag: $1" >&2; exit 1 ;;
|
||||
*) [ -z "$BRANCH" ] && BRANCH="$1" || { echo "unexpected extra arg: $1" >&2; exit 1; }; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$BRANCH" ] || { echo "usage: fm-gitea-pr.sh <branch> [--title <t>] [--body <b>] [--base <b>]" >&2; exit 1; }
|
||||
[ -z "$TITLE" ] && TITLE="$BRANCH"
|
||||
|
||||
command -v tea >/dev/null 2>&1 || { echo "error: tea not installed. brew install tea" >&2; exit 1; }
|
||||
|
||||
# Parse the Gitea owner/repo from the origin remote URL.
|
||||
# Handles http://user:token@host:port/owner/repo(.git) and https://host/owner/repo.git
|
||||
REMOTE=$(git remote get-url origin 2>/dev/null) || { echo "error: no git origin remote" >&2; exit 1; }
|
||||
# Strip credentials and .git suffix, then take the path after the host.
|
||||
PATH_PART=${REMOTE#*://}
|
||||
PATH_PART=${PATH_PART#*@} # drop user:pass@
|
||||
PATH_PART=${PATH_PART#*/} # drop host:port/ (first slash)
|
||||
PATH_PART=${PATH_PART%.git}
|
||||
OWNER_REPO=${PATH_PART}
|
||||
|
||||
case "$OWNER_REPO" in
|
||||
*/*) : ;; # owner/repo form
|
||||
*) echo "error: could not parse owner/repo from remote: $REMOTE" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Push the branch to origin (Gitea). --push-branch uses the configured remote.
|
||||
git push -u origin "$BRANCH" 2>&1 || { echo "error: git push failed for branch $BRANCH" >&2; exit 1; }
|
||||
|
||||
# Open the PR via tea. tea resolves the login by host automatically.
|
||||
PR_URL=$(tea pulls create \
|
||||
--repo "$OWNER_REPO" \
|
||||
--head "$BRANCH" \
|
||||
--base "$BASE" \
|
||||
--title "$TITLE" \
|
||||
--description "$BODY" 2>&1) || {
|
||||
echo "error: tea pulls create failed" >&2
|
||||
echo "$PR_URL" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# tea prints the PR URL on success; echo it for the caller.
|
||||
echo "$PR_URL"
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Launch OpenCode 2 with an isolated config so it cannot rewrite
|
||||
# ~/.config/opencode/opencode.json (OpenCode 1 / firstmate).
|
||||
set -euo pipefail
|
||||
|
||||
bin="${OPENCODE2_BIN:-/opt/homebrew/bin/opencode2}"
|
||||
cfg="${OPENCODE_CONFIG:-$HOME/.config/opencode2/opencode.json}"
|
||||
|
||||
if [ ! -x "$bin" ]; then
|
||||
echo "oc2: opencode2 not installed. Rebuild, or run:" >&2
|
||||
echo " /opt/homebrew/bin/npm install -g --allow-scripts=@opencode-ai/cli @opencode-ai/cli@beta" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -e "$cfg" ]; then
|
||||
echo "oc2: missing config $cfg (managed by home.nix)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export OPENCODE_CONFIG="$cfg"
|
||||
exec "$bin" "$@"
|
||||
Reference in New Issue
Block a user