@zerwiz/ymir 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/config/app-repos.yaml +38 -0
- package/.claude/settings.json +31 -0
- package/.codex/hooks.json +43 -0
- package/.cursor/hooks.json +33 -0
- package/.pi/extensions/README.md +39 -0
- package/.pi/extensions/gna-pi-watch.ts +15 -0
- package/.pi/extensions/lib/ro-assistant-layout.ts +96 -0
- package/.pi/extensions/lib/ro-operational-user-layout.ts +145 -0
- package/.pi/extensions/lib/ro-visibility.ts +99 -0
- package/.pi/extensions/lib/ro-working-ship.ts +243 -0
- package/.pi/extensions/lib/rodd-operational-input.ts +62 -0
- package/.pi/extensions/lib/skuld-branch-dispatch.ts +252 -0
- package/.pi/extensions/lib/skuld-branch-model-picker.ts +77 -0
- package/.pi/extensions/lib/vordr-sessionstart-supervisor.mjs +48 -0
- package/.pi/extensions/ro.ts +11 -0
- package/.pi/extensions/skuld-branch-supervision.ts +11 -0
- package/.pi/extensions/syn-turnend-guard.ts +11 -0
- package/.pi/mcp.json +16 -0
- package/.pi/mcp.json.example +11 -0
- package/.pi/npm/.gitignore +2 -0
- package/.pi/settings.json +8 -0
- package/.pi/shared/extensions/gna-pi-watch.ts +750 -0
- package/.pi/shared/extensions/herdr-agent-state.ts +257 -0
- package/.pi/shared/extensions/open-editor.ts +509 -0
- package/.pi/shared/extensions/ro.ts +504 -0
- package/.pi/shared/extensions/skuld-branch-supervision.ts +1535 -0
- package/.pi/shared/extensions/syn-turnend-guard.ts +683 -0
- package/.pi/shared/extensions/todo.ts +133 -0
- package/.pi/shared/extensions/ymir-subagents.ts +287 -0
- package/.pi/shared/extensions/ymir-well.ts +130 -0
- package/CHANGELOG.md +23 -0
- package/assets/Ymir.md +4654 -0
- package/assets/healer.html +717 -0
- package/assets/icon-family/family-sheet.png +0 -0
- package/assets/icon-family/hlidskjalf.svg +1 -0
- package/assets/icon-family/sessrumnir.svg +1 -0
- package/assets/icon-family/smidja.svg +1 -0
- package/assets/icon-family/ymir.svg +1 -0
- package/assets/mimirswell.html +880 -0
- package/assets/mock/README.md +16 -0
- package/assets/mock/feeds.ts +175 -0
- package/assets/mock/mock.ts +408 -0
- package/assets/reference/docs/agent-control.md +122 -0
- package/assets/reference/docs/apodex/Appdex_as_Orchestrator_Planner_Reviewer_Assessment.md +176 -0
- package/assets/reference/docs/apodex/Final Report(1).md +198 -0
- package/assets/reference/docs/apodex/Final Report(2).md +54 -0
- package/assets/reference/docs/apodex/Final Report(3).md +40 -0
- package/assets/reference/docs/apodex/Final Report(4).md +234 -0
- package/assets/reference/docs/apodex/Final Report(5).md +13 -0
- package/assets/reference/docs/apodex/Final Report.md +82 -0
- package/assets/reference/docs/architecture.md +392 -0
- package/assets/reference/docs/arm-pretool-check.md +247 -0
- package/assets/reference/docs/calm-mode-feasibility.md +541 -0
- package/assets/reference/docs/calm.md +59 -0
- package/assets/reference/docs/captain-hold-lifecycle.md +96 -0
- package/assets/reference/docs/cd-guard.md +165 -0
- package/assets/reference/docs/cmux-backend.md +133 -0
- package/assets/reference/docs/codex-app-backend.md +57 -0
- package/assets/reference/docs/configuration.md +924 -0
- package/assets/reference/docs/documentation-audiences.json +468 -0
- package/assets/reference/docs/documentation-audiences.md +28 -0
- package/assets/reference/docs/examples/crew-dispatch.json +26 -0
- package/assets/reference/docs/examples/process-event-extension/file-signal.mjs +96 -0
- package/assets/reference/docs/examples/process-event-extension/firstmate-extension.json +15 -0
- package/assets/reference/docs/examples/watched-tools.json +24 -0
- package/assets/reference/docs/examples/wedge-alarm +22 -0
- package/assets/reference/docs/extension-bindings.md +237 -0
- package/assets/reference/docs/fm-test-isolation-proof.json +37 -0
- package/assets/reference/docs/fm-test-isolation-proof.md +149 -0
- package/assets/reference/docs/fm-test-portable-shards.md +118 -0
- package/assets/reference/docs/gitlab-merge-watch.md +275 -0
- package/assets/reference/docs/herdr-backend.md +346 -0
- package/assets/reference/docs/orca-backend.md +84 -0
- package/assets/reference/docs/pi-supervision-branch-poster.svg +125 -0
- package/assets/reference/docs/pi-supervision-branch.md +96 -0
- package/assets/reference/docs/plans/agentic-engineering-workflow.md +395 -0
- package/assets/reference/docs/plans/agentic-factory-implementation-plan.md +615 -0
- package/assets/reference/docs/plans/command-factory-enhancement-plan.md +420 -0
- package/assets/reference/docs/plans/phase-1-layer3-program-design.md +134 -0
- package/assets/reference/docs/plans/phase-2-vertical-slices.md +100 -0
- package/assets/reference/docs/plans/phase-3-measurable-goals.md +106 -0
- package/assets/reference/docs/plans/phase-4-context-budget.md +106 -0
- package/assets/reference/docs/plans/phase-5-pre-mortem-confidence.md +100 -0
- package/assets/reference/docs/plans/phase-6-incident-to-agent.md +109 -0
- package/assets/reference/docs/remote-secondmates.md +268 -0
- package/assets/reference/docs/scripts.md +146 -0
- package/assets/reference/docs/sessionstart-nudge.md +115 -0
- package/assets/reference/docs/subagent-guard.md +383 -0
- package/assets/reference/docs/supervision-protocols/claude.md +27 -0
- package/assets/reference/docs/supervision-protocols/codex.md +16 -0
- package/assets/reference/docs/supervision-protocols/cursor.md +31 -0
- package/assets/reference/docs/supervision-protocols/grok.md +39 -0
- package/assets/reference/docs/supervision-protocols/opencode.md +17 -0
- package/assets/reference/docs/supervision-protocols/pi.md +33 -0
- package/assets/reference/docs/supervision-protocols/unknown.md +13 -0
- package/assets/reference/docs/tmux-backend.md +115 -0
- package/assets/reference/docs/trace-context.md +117 -0
- package/assets/reference/docs/turnend-guard.md +170 -0
- package/assets/reference/docs/verification/dispatch-auth.md +204 -0
- package/assets/reference/docs/verification/muse.md +221 -0
- package/assets/reference/docs/verification/process-event-sources.md +208 -0
- package/assets/reference/docs/verification/public-followup.md +119 -0
- package/assets/reference/docs/verification/runtime-backends.md +1022 -0
- package/assets/reference/docs/verification/stow-memory.md +53 -0
- package/assets/reference/docs/verification/supervision.md +534 -0
- package/assets/reference/docs/verification/trace-context.md +31 -0
- package/assets/reference/docs/voice-relay.md +295 -0
- package/assets/reference/docs/watcher-continuity.md +119 -0
- package/assets/reference/docs/wedge-alarm.md +39 -0
- package/assets/reference/docs/zellij-backend.md +114 -0
- package/assets/reference/index.html +1172 -0
- package/assets/reference/skills/afk/SKILL.md +241 -0
- package/assets/reference/skills/ahoy/SKILL.md +56 -0
- package/assets/reference/skills/ask-user-authority/SKILL.md +56 -0
- package/assets/reference/skills/bearings/SKILL.md +161 -0
- package/assets/reference/skills/bearings/assets/board-template.html +731 -0
- package/assets/reference/skills/bootstrap-diagnostics/SKILL.md +68 -0
- package/assets/reference/skills/captain-hold-lifecycle/SKILL.md +54 -0
- package/assets/reference/skills/decision-hold-lifecycle/SKILL.md +15 -0
- package/assets/reference/skills/diagnostic-reasoning/SKILL.md +53 -0
- package/assets/reference/skills/firstmate-codexapp/SKILL.md +110 -0
- package/assets/reference/skills/firstmate-codexapp/agents/openai.yaml +4 -0
- package/assets/reference/skills/firstmate-coding-guidelines/SKILL.md +127 -0
- package/assets/reference/skills/firstmate-orca/SKILL.md +92 -0
- package/assets/reference/skills/fmx-respond/SKILL.md +283 -0
- package/assets/reference/skills/harness-adapters/SKILL.md +95 -0
- package/assets/reference/skills/harness-adapters/references/common/control-and-recovery.md +37 -0
- package/assets/reference/skills/harness-adapters/references/common/dispatch.md +32 -0
- package/assets/reference/skills/harness-adapters/references/common/model-and-effort.md +42 -0
- package/assets/reference/skills/harness-adapters/references/common/primary-hooks.md +40 -0
- package/assets/reference/skills/harness-adapters/references/harness/claude.md +55 -0
- package/assets/reference/skills/harness-adapters/references/harness/codex.md +43 -0
- package/assets/reference/skills/harness-adapters/references/harness/cursor.md +75 -0
- package/assets/reference/skills/harness-adapters/references/harness/grok.md +69 -0
- package/assets/reference/skills/harness-adapters/references/harness/kimi.md +51 -0
- package/assets/reference/skills/harness-adapters/references/harness/muse.md +70 -0
- package/assets/reference/skills/harness-adapters/references/harness/opencode.md +42 -0
- package/assets/reference/skills/harness-adapters/references/harness/pi.md +56 -0
- package/assets/reference/skills/process-event-sources/SKILL.md +137 -0
- package/assets/reference/skills/project-management/SKILL.md +92 -0
- package/assets/reference/skills/quota-array-dispatch/SKILL.md +131 -0
- package/assets/reference/skills/secondmate-provisioning/SKILL.md +254 -0
- package/assets/reference/skills/stow/SKILL.md +309 -0
- package/assets/reference/skills/stuck-crewmate-recovery/SKILL.md +54 -0
- package/assets/reference/skills/updatefirstmate/SKILL.md +64 -0
- package/assets/skills/README.md +5 -0
- package/assets/yggdrasil.html +649 -0
- package/assets/ymir-banner-01.png +0 -0
- package/assets/ymir-banner-02.png +0 -0
- package/assets/ymir-banner-03.png +0 -0
- package/assets/ymir-banner-04.png +0 -0
- package/assets/ymir-banner-05.png +0 -0
- package/assets/ymir-banner-06.png +0 -0
- package/assets/ymir-emblem-darksteel.svg +63 -0
- package/assets/ymir-emblem-forged.svg +55 -0
- package/assets/ymir-emblem-slate.svg +55 -0
- package/assets/ymir-mark-algiz-anvil.svg +9 -0
- package/assets/ymir-stave.svg +53 -0
- package/assets/ymir.html +563 -0
- package/bin/ymir-install.sh +6 -1
- package/data/learnings.md +33 -0
- package/data/local-models.md +34 -0
- package/data/operator.md +15 -0
- package/data/projects.md +28 -0
- package/data/realm.md +1 -0
- package/deploy/Containerfile +27 -0
- package/deploy/README.md +104 -0
- package/deploy/compose/compose.yaml +45 -0
- package/deploy/env.example +30 -0
- package/deploy/quadlet/ymir.container +57 -0
- package/package.json +10 -8
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// Brokk's home-persistent Pi transcript presentation toggle.
|
|
2
|
+
//
|
|
3
|
+
// Verified against Pi 0.81.1, 0.82.0, and 0.84.4, which expose built-in ToolDefinitions, per-slot
|
|
4
|
+
// renderers, renderShell: "self", session_start replacement reasons, agent_start and
|
|
5
|
+
// agent_settled, ExtensionUIContext.setToolsExpanded(), setWorkingVisible(), setWidget()
|
|
6
|
+
// with a disposable component smidja, and setHiddenThinkingLabel().
|
|
7
|
+
// ./lib/ro-working-ship.ts owns the animated working presentation this file
|
|
8
|
+
// installs. The focused tests pin those assumptions but never reject a
|
|
9
|
+
// newer Pi solely for its version. The collapsed-thinking and operational-user
|
|
10
|
+
// presentation adapters probe the exact API they patch and degrade independently with a
|
|
11
|
+
// diagnostic (see installCalmPresentationAdapter below) if a future Pi removes it; Pi
|
|
12
|
+
// still exposes no global renderer for arbitrary built-in or custom rows.
|
|
13
|
+
// docs/configuration.md owns the home-local Ró preference contract.
|
|
14
|
+
//
|
|
15
|
+
// Pi has one first-registration-wins ToolDefinition per tool name, with no merge or
|
|
16
|
+
// unregister operation. Keep Ró-off registration empty; keep Ró-on load-time
|
|
17
|
+
// registration synchronous because restored rows capture the registry before
|
|
18
|
+
// session_start; and collision-check only the later first-activation path, when
|
|
19
|
+
// getAllTools() is reliable. docs/ro-mode-feasibility.md owns the Pi-source evidence
|
|
20
|
+
// and docs/ro.md owns the user-facing behavior and non-retroactive first-toggle bound.
|
|
21
|
+
import { randomUUID } from "node:crypto";
|
|
22
|
+
import {
|
|
23
|
+
mkdirSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
realpathSync,
|
|
26
|
+
renameSync,
|
|
27
|
+
rmSync,
|
|
28
|
+
writeFileSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import { dirname, resolve } from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import type {
|
|
33
|
+
ExtensionAPI,
|
|
34
|
+
ExtensionUIContext,
|
|
35
|
+
ToolDefinition,
|
|
36
|
+
ToolInfo,
|
|
37
|
+
ToolRenderResultOptions,
|
|
38
|
+
} from "@earendil-works/pi-coding-agent";
|
|
39
|
+
import {
|
|
40
|
+
createBashToolDefinition,
|
|
41
|
+
createEditToolDefinition,
|
|
42
|
+
createFindToolDefinition,
|
|
43
|
+
createGrepToolDefinition,
|
|
44
|
+
createLsToolDefinition,
|
|
45
|
+
createReadToolDefinition,
|
|
46
|
+
createWriteToolDefinition,
|
|
47
|
+
} from "@earendil-works/pi-coding-agent";
|
|
48
|
+
import { Box, Container, getKeybindings, type Component } from "@earendil-works/pi-tui";
|
|
49
|
+
import type { TSchema } from "typebox";
|
|
50
|
+
import { installRoAssistantLayout } from "./lib/ro-assistant-layout.ts";
|
|
51
|
+
import { installRoOperationalUserLayout } from "./lib/ro-operational-user-layout.ts";
|
|
52
|
+
import {
|
|
53
|
+
RO_WORKING_SHIP_WIDGET_KEY,
|
|
54
|
+
createCalmWorkingShipAnimation,
|
|
55
|
+
createCalmWorkingShipWidget,
|
|
56
|
+
} from "./lib/ro-working-ship.ts";
|
|
57
|
+
import {
|
|
58
|
+
roPresentationHides,
|
|
59
|
+
calmPresentationIsActive,
|
|
60
|
+
RO_PRESENTATION_EVENT,
|
|
61
|
+
registerBrokkSyntheticPresentation,
|
|
62
|
+
setCalmPresentation,
|
|
63
|
+
setCalmStockExportRendering,
|
|
64
|
+
} from "./lib/ro-visibility.ts";
|
|
65
|
+
|
|
66
|
+
type DefinitionFactory<TParams extends TSchema, TDetails, TState> = (
|
|
67
|
+
cwd: string,
|
|
68
|
+
) => ToolDefinition<TParams, TDetails, TState>;
|
|
69
|
+
|
|
70
|
+
type RenderContext<TParams extends TSchema, TDetails, TState> = Parameters<
|
|
71
|
+
NonNullable<ToolDefinition<TParams, TDetails, TState>["renderCall"]>
|
|
72
|
+
>[2];
|
|
73
|
+
|
|
74
|
+
type RenderArgs<TParams extends TSchema, TDetails, TState> = Parameters<
|
|
75
|
+
NonNullable<ToolDefinition<TParams, TDetails, TState>["renderCall"]>
|
|
76
|
+
>[0];
|
|
77
|
+
|
|
78
|
+
type RenderTheme<TParams extends TSchema, TDetails, TState> = Parameters<
|
|
79
|
+
NonNullable<ToolDefinition<TParams, TDetails, TState>["renderCall"]>
|
|
80
|
+
>[1];
|
|
81
|
+
|
|
82
|
+
type RenderResult<TParams extends TSchema, TDetails, TState> = Parameters<
|
|
83
|
+
NonNullable<ToolDefinition<TParams, TDetails, TState>["renderResult"]>
|
|
84
|
+
>[0];
|
|
85
|
+
|
|
86
|
+
type StandardShellState = {
|
|
87
|
+
shell?: Box;
|
|
88
|
+
call?: Component;
|
|
89
|
+
result?: Component;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const extensionFile = fileURLToPath(import.meta.url);
|
|
93
|
+
const extensionDir = dirname(extensionFile);
|
|
94
|
+
const root = resolve(extensionDir, "../..");
|
|
95
|
+
|
|
96
|
+
// Resolves symlinks before comparing tool-ownership identity below: sourceInfo.path
|
|
97
|
+
// values come from independent path-resolution code paths (this module's own
|
|
98
|
+
// import.meta.url vs. Pi's extension loader), and macOS alone symlinks /tmp and /var
|
|
99
|
+
// to /private/..., so lexical string comparison alone spuriously reads a symlinked
|
|
100
|
+
// self-path as a foreign one. Falls back to the raw path for synthetic, non-file
|
|
101
|
+
// sourceInfo paths such as "<builtin:read>" or "<inline>", which realpathSync rejects.
|
|
102
|
+
const realpathOrSelf = (path: string): string => {
|
|
103
|
+
try {
|
|
104
|
+
return realpathSync(path);
|
|
105
|
+
} catch {
|
|
106
|
+
return path;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const extensionRealFile = realpathOrSelf(extensionFile);
|
|
110
|
+
|
|
111
|
+
// Each presentation adapter probes the exact Pi API it patches. If a future Pi removes
|
|
112
|
+
// that API, only the affected adapter degrades; the rest of Ró keeps working.
|
|
113
|
+
function installCalmPresentationAdapter(name: string, install: () => void): void {
|
|
114
|
+
try {
|
|
115
|
+
install();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
118
|
+
console.error(`Brokk Ró: ${name} presentation adapter unavailable, skipping. ${reason}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default function (pi: ExtensionAPI) {
|
|
123
|
+
installCalmPresentationAdapter("collapsed-thinking", installRoAssistantLayout);
|
|
124
|
+
installCalmPresentationAdapter("operational-user-row", installRoOperationalUserLayout);
|
|
125
|
+
|
|
126
|
+
let exportRendering = false;
|
|
127
|
+
let removeTerminalInputHandler: (() => void) | undefined;
|
|
128
|
+
// One logical agent run, tracked from agent_start through agent_settled rather than
|
|
129
|
+
// from turns or tool calls, so the boat never flickers between tool calls, automatic
|
|
130
|
+
// continuations, retries, or compaction that stay inside the same run.
|
|
131
|
+
let agentRunActive = false;
|
|
132
|
+
let workingShipShown = false;
|
|
133
|
+
// One animation instance per extension lifetime. Hiding the working widget freezes
|
|
134
|
+
// this state; the next working period resumes it. session_start resets it so a fresh
|
|
135
|
+
// Pi session starts at the normal initial position. Never module-global.
|
|
136
|
+
const workingShipAnimation = createCalmWorkingShipAnimation();
|
|
137
|
+
|
|
138
|
+
// Single owner of Ró's working-row presentation choice. The widget is only created
|
|
139
|
+
// or removed on a real transition, so repeated starts cannot duplicate its timer.
|
|
140
|
+
const applyWorkingPresentation = (
|
|
141
|
+
ui: ExtensionUIContext,
|
|
142
|
+
forceStockVisibility = false,
|
|
143
|
+
): void => {
|
|
144
|
+
const showShip = agentRunActive && calmPresentationIsActive();
|
|
145
|
+
if (showShip !== workingShipShown) {
|
|
146
|
+
workingShipShown = showShip;
|
|
147
|
+
ui.setWidget(
|
|
148
|
+
RO_WORKING_SHIP_WIDGET_KEY,
|
|
149
|
+
showShip
|
|
150
|
+
? (tui) => createCalmWorkingShipWidget(tui, workingShipAnimation)
|
|
151
|
+
: undefined,
|
|
152
|
+
);
|
|
153
|
+
ui.setWorkingVisible(!showShip);
|
|
154
|
+
} else if (forceStockVisibility && !showShip) {
|
|
155
|
+
ui.setWorkingVisible(true);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const fmHome = process.env.BROKK_HOME || process.env.BROKK_ROOT_OVERRIDE || root;
|
|
160
|
+
const configDirectory = process.env.BROKK_CONFIG_OVERRIDE || resolve(fmHome, "config");
|
|
161
|
+
const stateDirectory = process.env.BROKK_STATE_OVERRIDE || resolve(fmHome, "state");
|
|
162
|
+
// Ró is a PER-USER preference: it lives in the gitignored state dir so toggling
|
|
163
|
+
// it never dirties the tracked tree, and YMIR_RO/BROKK_RO may set the default.
|
|
164
|
+
// The legacy tracked `config/ro` is still read (upgrade path) but never written.
|
|
165
|
+
const calmPreferencePath = resolve(stateDirectory, "ro");
|
|
166
|
+
const legacyCalmPath = resolve(configDirectory, "ro");
|
|
167
|
+
const truthy = (v: string): boolean => v === "on" || v === "max";
|
|
168
|
+
const loadCalmPreference = (): boolean => {
|
|
169
|
+
for (const p of [calmPreferencePath, legacyCalmPath]) {
|
|
170
|
+
try {
|
|
171
|
+
const stored = readFileSync(p, "utf8").trim();
|
|
172
|
+
if (stored) return truthy(stored);
|
|
173
|
+
} catch {
|
|
174
|
+
/* try the next source */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const env = (process.env.YMIR_RO || process.env.BROKK_RO || "").trim().toLowerCase();
|
|
178
|
+
if (env === "on" || env === "max") return true;
|
|
179
|
+
if (env === "off") return false;
|
|
180
|
+
return false;
|
|
181
|
+
};
|
|
182
|
+
const persistCalmPreference = (active: boolean): void => {
|
|
183
|
+
mkdirSync(dirname(calmPreferencePath), { recursive: true });
|
|
184
|
+
const temporaryPath = `${calmPreferencePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
185
|
+
try {
|
|
186
|
+
writeFileSync(temporaryPath, active ? "on\n" : "off\n", {
|
|
187
|
+
encoding: "utf8",
|
|
188
|
+
flag: "wx",
|
|
189
|
+
mode: 0o600,
|
|
190
|
+
});
|
|
191
|
+
renameSync(temporaryPath, calmPreferencePath);
|
|
192
|
+
} finally {
|
|
193
|
+
rmSync(temporaryPath, { force: true });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const publishPresentationState = (): void => {
|
|
198
|
+
pi.events.emit(RO_PRESENTATION_EVENT, {
|
|
199
|
+
active: calmPresentationIsActive(),
|
|
200
|
+
stockExportRendering: exportRendering,
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
registerBrokkSyntheticPresentation(pi);
|
|
205
|
+
|
|
206
|
+
// Every on-screen tool row Ró currently presents, keyed by the row-local state Pi
|
|
207
|
+
// hands its render slots, so Ró can repaint exactly those rows without touching
|
|
208
|
+
// Pi's transcript. Pi can re-render a row at any time - the built-in edit row
|
|
209
|
+
// invalidates itself once its diff is ready - so a row can be redrawn during the
|
|
210
|
+
// window where /export forces stock rendering and keep that stock content
|
|
211
|
+
// afterwards. Rows Pi's exporter renders are excluded: those use throwaway state
|
|
212
|
+
// and never appear on screen. Cleared per session lifetime, which rebuilds the rows.
|
|
213
|
+
const calmToolRowRepaints = new Map<object, () => void>();
|
|
214
|
+
const rememberCalmToolRow = (state: object, invalidate: unknown): void => {
|
|
215
|
+
if (exportRendering || typeof invalidate !== "function") return;
|
|
216
|
+
calmToolRowRepaints.set(state, invalidate as () => void);
|
|
217
|
+
};
|
|
218
|
+
const repaintCalmToolRows = (): void => {
|
|
219
|
+
for (const invalidate of calmToolRowRepaints.values()) invalidate();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
function wrapBuiltIn<TParams extends TSchema, TDetails, TState>(
|
|
223
|
+
smidja: DefinitionFactory<TParams, TDetails, TState>,
|
|
224
|
+
): ToolDefinition<TParams, TDetails, TState> {
|
|
225
|
+
const definitions = new Map<string, ToolDefinition<TParams, TDetails, TState>>();
|
|
226
|
+
const definitionFor = (cwd: string): ToolDefinition<TParams, TDetails, TState> => {
|
|
227
|
+
let definition = definitions.get(cwd);
|
|
228
|
+
if (!definition) {
|
|
229
|
+
definition = smidja(cwd);
|
|
230
|
+
definitions.set(cwd, definition);
|
|
231
|
+
}
|
|
232
|
+
return definition;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const original = definitionFor(process.cwd());
|
|
236
|
+
const originalRenderCall = original.renderCall;
|
|
237
|
+
const originalRenderResult = original.renderResult;
|
|
238
|
+
const originalSelfShell = original.renderShell === "self";
|
|
239
|
+
const standardShells = new WeakMap<object, StandardShellState>();
|
|
240
|
+
|
|
241
|
+
if (!originalRenderCall || !originalRenderResult) {
|
|
242
|
+
throw new Error(`Brokk ro mode requires both render slots for Pi built-in tool ${original.name}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const shellStateFor = (
|
|
246
|
+
context: RenderContext<TParams, TDetails, TState>,
|
|
247
|
+
): StandardShellState => {
|
|
248
|
+
const rowState = context.state as object;
|
|
249
|
+
let shellState = standardShells.get(rowState);
|
|
250
|
+
if (!shellState) {
|
|
251
|
+
shellState = {};
|
|
252
|
+
standardShells.set(rowState, shellState);
|
|
253
|
+
}
|
|
254
|
+
return shellState;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const refreshStandardShell = (
|
|
258
|
+
state: StandardShellState,
|
|
259
|
+
theme: RenderTheme<TParams, TDetails, TState>,
|
|
260
|
+
context: RenderContext<TParams, TDetails, TState>,
|
|
261
|
+
): Box => {
|
|
262
|
+
const background = context.isPartial
|
|
263
|
+
? (text: string) => theme.bg("toolPendingBg", text)
|
|
264
|
+
: context.isError
|
|
265
|
+
? (text: string) => theme.bg("toolErrorBg", text)
|
|
266
|
+
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
267
|
+
const shell = state.shell ?? new Box(1, 1, background);
|
|
268
|
+
state.shell = shell;
|
|
269
|
+
shell.setBgFn(background);
|
|
270
|
+
shell.clear();
|
|
271
|
+
if (state.call) shell.addChild(state.call);
|
|
272
|
+
if (state.result) shell.addChild(state.result);
|
|
273
|
+
return shell;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
...original,
|
|
278
|
+
renderShell: "self",
|
|
279
|
+
|
|
280
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
281
|
+
return definitionFor(ctx.cwd).execute(toolCallId, params, signal, onUpdate, ctx);
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
renderCall(
|
|
285
|
+
args: RenderArgs<TParams, TDetails, TState>,
|
|
286
|
+
theme: RenderTheme<TParams, TDetails, TState>,
|
|
287
|
+
context: RenderContext<TParams, TDetails, TState>,
|
|
288
|
+
) {
|
|
289
|
+
rememberCalmToolRow(context.state as object, context.invalidate);
|
|
290
|
+
if (exportRendering) return originalRenderCall(args, theme, context);
|
|
291
|
+
if (roPresentationHides("assistant-tool-call")) return new Container();
|
|
292
|
+
if (originalSelfShell) return originalRenderCall(args, theme, context);
|
|
293
|
+
|
|
294
|
+
const state = shellStateFor(context);
|
|
295
|
+
state.call = originalRenderCall(args, theme, {
|
|
296
|
+
...context,
|
|
297
|
+
lastComponent: state.call,
|
|
298
|
+
});
|
|
299
|
+
return refreshStandardShell(state, theme, context);
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
renderResult(
|
|
303
|
+
result: RenderResult<TParams, TDetails, TState>,
|
|
304
|
+
options: ToolRenderResultOptions,
|
|
305
|
+
theme: RenderTheme<TParams, TDetails, TState>,
|
|
306
|
+
context: RenderContext<TParams, TDetails, TState>,
|
|
307
|
+
) {
|
|
308
|
+
rememberCalmToolRow(context.state as object, context.invalidate);
|
|
309
|
+
if (exportRendering) return originalRenderResult(result, options, theme, context);
|
|
310
|
+
if (roPresentationHides("tool-result")) return new Container();
|
|
311
|
+
if (originalSelfShell) return originalRenderResult(result, options, theme, context);
|
|
312
|
+
|
|
313
|
+
const state = shellStateFor(context);
|
|
314
|
+
state.result = originalRenderResult(result, options, theme, {
|
|
315
|
+
...context,
|
|
316
|
+
lastComponent: state.result,
|
|
317
|
+
});
|
|
318
|
+
refreshStandardShell(state, theme, context);
|
|
319
|
+
return new Container();
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Each wrapBuiltIn() call below has its own concrete TParams/TDetails/TState; the
|
|
325
|
+
// array holding all seven has no single sound instantiation, so it is typed the same
|
|
326
|
+
// way Pi's own ToolDefinition consumers erase this (any, any, any).
|
|
327
|
+
const wrappedBuiltIns: ToolDefinition<any, any, any>[] = [
|
|
328
|
+
wrapBuiltIn(createReadToolDefinition),
|
|
329
|
+
wrapBuiltIn(createBashToolDefinition),
|
|
330
|
+
wrapBuiltIn(createEditToolDefinition),
|
|
331
|
+
wrapBuiltIn(createWriteToolDefinition),
|
|
332
|
+
wrapBuiltIn(createGrepToolDefinition),
|
|
333
|
+
wrapBuiltIn(createFindToolDefinition),
|
|
334
|
+
wrapBuiltIn(createLsToolDefinition),
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
// True once this extension has handled built-in registration for its lifetime:
|
|
338
|
+
// either all seven synchronously at load, or only the uncontested subset during
|
|
339
|
+
// first activation.
|
|
340
|
+
let builtInsRegistered = false;
|
|
341
|
+
|
|
342
|
+
// Gate on Ró already being on at load time. This must stay synchronous and
|
|
343
|
+
// unconditional here (see file header): a foreign-claim check is not reachable at
|
|
344
|
+
// this point, while deferral would make restored rows capture the wrong definition.
|
|
345
|
+
// A Ró-off session or reload registers nothing and creates no collision exposure.
|
|
346
|
+
if (loadCalmPreference()) {
|
|
347
|
+
for (const tool of wrappedBuiltIns) pi.registerTool(tool);
|
|
348
|
+
builtInsRegistered = true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Which of the 7 built-ins are currently owned by a different, non-builtin
|
|
352
|
+
// extension. Only safe to call once every extension has finished loading (see file
|
|
353
|
+
// header); never call this during the smidja's own synchronous execution above.
|
|
354
|
+
function contestedBuiltIns(): ToolDefinition<any, any, any>[] {
|
|
355
|
+
let registered: ToolInfo[];
|
|
356
|
+
try {
|
|
357
|
+
registered = pi.getAllTools();
|
|
358
|
+
} catch (error) {
|
|
359
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
360
|
+
console.error(`Brokk Ró: built-in ownership check unavailable, claiming every built-in unconditionally. ${reason}`);
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
return wrappedBuiltIns.filter((tool) => {
|
|
364
|
+
const owner = registered.find((info) => info.name === tool.name)?.sourceInfo;
|
|
365
|
+
return owner !== undefined && owner.source !== "builtin" && realpathOrSelf(owner.path) !== extensionRealFile;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// The first time Ró turns on in a session that started off, claim every
|
|
370
|
+
// uncontested built-in and leave each contested tool and its owning extension
|
|
371
|
+
// untouched. Tell the user which built-in Ró could not take over, since Ró's
|
|
372
|
+
// presentation does not apply to it.
|
|
373
|
+
function activateBuiltInsIfNeeded(ui: ExtensionUIContext): void {
|
|
374
|
+
if (builtInsRegistered) return;
|
|
375
|
+
const contested = contestedBuiltIns();
|
|
376
|
+
const contestedNames = new Set(contested.map((tool) => tool.name));
|
|
377
|
+
for (const tool of wrappedBuiltIns) {
|
|
378
|
+
if (!contestedNames.has(tool.name)) pi.registerTool(tool);
|
|
379
|
+
}
|
|
380
|
+
builtInsRegistered = true;
|
|
381
|
+
if (contested.length === 0) return;
|
|
382
|
+
const names = contested.map((tool) => `"${tool.name}"`).join(", ");
|
|
383
|
+
const plural = contested.length > 1;
|
|
384
|
+
ui.notify(
|
|
385
|
+
`Brokk Ró: the ${names} built-in tool${plural ? "s are" : " is"} already provided by another extension, so Ró may not fully function for ${plural ? "them" : "it"} this session.`,
|
|
386
|
+
"warning",
|
|
387
|
+
);
|
|
388
|
+
for (const tool of contested) {
|
|
389
|
+
console.error(`Brokk Ró: skipped claiming built-in "${tool.name}" because another extension already owns it.`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Backstop for the one case activateBuiltInsIfNeeded cannot reach: Ró registered
|
|
394
|
+
// unconditionally at load time because it was already on, without any chance to
|
|
395
|
+
// check for a foreign claim first, so it can still silently lose a name to an
|
|
396
|
+
// earlier-loaded extension. Runs on every session_start reason because a reload
|
|
397
|
+
// rebuilds every extension's registrations from scratch, so last session's clean
|
|
398
|
+
// bill of health does not carry over.
|
|
399
|
+
function reportBuiltInLosses(): void {
|
|
400
|
+
if (!builtInsRegistered) return;
|
|
401
|
+
let registered: ToolInfo[];
|
|
402
|
+
try {
|
|
403
|
+
registered = pi.getAllTools();
|
|
404
|
+
} catch (error) {
|
|
405
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
406
|
+
console.error(`Brokk Ró: built-in ownership check unavailable. ${reason}`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
for (const tool of wrappedBuiltIns) {
|
|
410
|
+
const owner = registered.find((info) => info.name === tool.name)?.sourceInfo;
|
|
411
|
+
if (owner && owner.source !== "builtin" && realpathOrSelf(owner.path) !== extensionRealFile) {
|
|
412
|
+
console.error(
|
|
413
|
+
`Brokk Ró: another extension (${owner.path}) also claimed the built-in "${tool.name}" tool and won; Ró's presentation for it is unavailable this session.`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
pi.on("session_start", (_event, ctx) => {
|
|
420
|
+
reportBuiltInLosses();
|
|
421
|
+
calmToolRowRepaints.clear();
|
|
422
|
+
exportRendering = false;
|
|
423
|
+
setCalmPresentation(loadCalmPreference());
|
|
424
|
+
setCalmStockExportRendering(false);
|
|
425
|
+
publishPresentationState();
|
|
426
|
+
agentRunActive = false;
|
|
427
|
+
workingShipShown = false;
|
|
428
|
+
// A genuine new session lifetime starts the boat at the normal initial position.
|
|
429
|
+
workingShipAnimation.reset();
|
|
430
|
+
applyWorkingPresentation(ctx.ui, true);
|
|
431
|
+
ctx.ui.setHiddenThinkingLabel(calmPresentationIsActive() ? "" : undefined);
|
|
432
|
+
ctx.ui.setStatus("brokk-ro", undefined);
|
|
433
|
+
removeTerminalInputHandler?.();
|
|
434
|
+
removeTerminalInputHandler = ctx.ui.onTerminalInput((data) => {
|
|
435
|
+
if (!getKeybindings().matches(data, "tui.input.submit")) return undefined;
|
|
436
|
+
|
|
437
|
+
const input = ctx.ui.getEditorText().trim();
|
|
438
|
+
if (
|
|
439
|
+
input !== "/share" &&
|
|
440
|
+
input !== "/export" &&
|
|
441
|
+
!input.startsWith("/export ")
|
|
442
|
+
) {
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
exportRendering = true;
|
|
447
|
+
setCalmStockExportRendering(true);
|
|
448
|
+
publishPresentationState();
|
|
449
|
+
setTimeout(() => {
|
|
450
|
+
exportRendering = false;
|
|
451
|
+
setCalmStockExportRendering(false);
|
|
452
|
+
publishPresentationState();
|
|
453
|
+
// Repaint the rows Ró presents, never the whole transcript. Pi's export
|
|
454
|
+
// prints "Session exported to: <path>" immediately before this runs, and
|
|
455
|
+
// since Pi 0.83.0 setToolsExpanded() emits its own status line; consecutive
|
|
456
|
+
// status lines coalesce, so a tools-expanded round-trip here silently
|
|
457
|
+
// overwrote the confirmation and left the Allfather no record of where their
|
|
458
|
+
// export landed. Invalidating the rows individually repaints the same
|
|
459
|
+
// content with no status line of its own, and setStatus adds the redraw the
|
|
460
|
+
// rows that consult Ró live in render(), such as operational user rows,
|
|
461
|
+
// need without appending anything to the transcript.
|
|
462
|
+
repaintCalmToolRows();
|
|
463
|
+
ctx.ui.setStatus("brokk-ro", undefined);
|
|
464
|
+
}, 0);
|
|
465
|
+
return undefined;
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
470
|
+
agentRunActive = true;
|
|
471
|
+
applyWorkingPresentation(ctx.ui);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// agent_settled is emitted from a finally block, so it also covers abort and failure.
|
|
475
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
476
|
+
agentRunActive = false;
|
|
477
|
+
applyWorkingPresentation(ctx.ui);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
481
|
+
agentRunActive = false;
|
|
482
|
+
applyWorkingPresentation(ctx.ui);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
pi.registerCommand("ro", {
|
|
486
|
+
description: "Toggle Brokk's supported conversation-only transcript presentation.",
|
|
487
|
+
handler: async (_args, ctx) => {
|
|
488
|
+
const active = !calmPresentationIsActive();
|
|
489
|
+
persistCalmPreference(active);
|
|
490
|
+
setCalmPresentation(active);
|
|
491
|
+
if (active) activateBuiltInsIfNeeded(ctx.ui);
|
|
492
|
+
publishPresentationState();
|
|
493
|
+
applyWorkingPresentation(ctx.ui, true);
|
|
494
|
+
// Pi re-runs every assistant row's layout from this call even when the label is
|
|
495
|
+
// unchanged, which is what makes a toggle apply to rows already on screen.
|
|
496
|
+
ctx.ui.setHiddenThinkingLabel(active ? "" : undefined);
|
|
497
|
+
ctx.ui.setStatus("brokk-ro", undefined);
|
|
498
|
+
|
|
499
|
+
const expanded = ctx.ui.getToolsExpanded();
|
|
500
|
+
ctx.ui.setToolsExpanded(!expanded);
|
|
501
|
+
ctx.ui.setToolsExpanded(expanded);
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
}
|