@zhijiewang/openharness 2.1.0 → 2.3.0
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/README.md +4 -4
- package/dist/DeferredTool.js +3 -1
- package/dist/Tool.d.ts +1 -1
- package/dist/agents/roles.js +58 -62
- package/dist/commands/cybergotchi.d.ts +1 -1
- package/dist/commands/cybergotchi.js +30 -30
- package/dist/commands/index.js +288 -132
- package/dist/components/App.d.ts +1 -1
- package/dist/components/App.js +6 -6
- package/dist/components/CompanionFooter.d.ts +1 -1
- package/dist/components/CompanionFooter.js +6 -8
- package/dist/components/CybergotchiBubble.js +5 -5
- package/dist/components/CybergotchiPanel.d.ts +1 -1
- package/dist/components/CybergotchiPanel.js +7 -7
- package/dist/components/CybergotchiPanelConnected.js +2 -2
- package/dist/components/CybergotchiSetup.js +26 -24
- package/dist/components/CybergotchiSprite.d.ts +1 -1
- package/dist/components/CybergotchiSprite.js +8 -12
- package/dist/components/DiffView.d.ts +1 -1
- package/dist/components/DiffView.js +10 -10
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/InitWizard.js +65 -33
- package/dist/components/Markdown.js +2 -4
- package/dist/components/Messages.js +4 -4
- package/dist/components/PermissionPrompt.d.ts +1 -1
- package/dist/components/PermissionPrompt.js +15 -17
- package/dist/components/REPL.d.ts +1 -1
- package/dist/components/REPL.js +74 -49
- package/dist/components/Spinner.js +2 -2
- package/dist/components/TextInput.js +35 -29
- package/dist/components/ToolCallDisplay.js +3 -5
- package/dist/cybergotchi/bones.d.ts +1 -1
- package/dist/cybergotchi/bones.js +8 -8
- package/dist/cybergotchi/config.d.ts +2 -2
- package/dist/cybergotchi/config.js +13 -13
- package/dist/cybergotchi/events.d.ts +5 -5
- package/dist/cybergotchi/events.js +7 -7
- package/dist/cybergotchi/needs.d.ts +2 -2
- package/dist/cybergotchi/needs.js +7 -9
- package/dist/cybergotchi/personality.d.ts +2 -2
- package/dist/cybergotchi/personality.js +2 -2
- package/dist/cybergotchi/species.d.ts +1 -1
- package/dist/cybergotchi/species.js +145 -217
- package/dist/cybergotchi/speech.d.ts +2 -2
- package/dist/cybergotchi/speech.js +43 -43
- package/dist/cybergotchi/types.d.ts +4 -4
- package/dist/cybergotchi/types.js +26 -26
- package/dist/cybergotchi/useCybergotchi.d.ts +1 -1
- package/dist/cybergotchi/useCybergotchi.js +29 -25
- package/dist/git/index.js +11 -9
- package/dist/harness/checkpoints.js +29 -21
- package/dist/harness/config.d.ts +3 -3
- package/dist/harness/config.js +15 -9
- package/dist/harness/context-warning.d.ts +1 -1
- package/dist/harness/context-warning.js +1 -1
- package/dist/harness/cost.js +1 -1
- package/dist/harness/credentials.js +13 -13
- package/dist/harness/hooks.js +7 -5
- package/dist/harness/keybindings.js +20 -18
- package/dist/harness/marketplace.d.ts +3 -3
- package/dist/harness/marketplace.js +55 -42
- package/dist/harness/memory.d.ts +23 -5
- package/dist/harness/memory.js +142 -41
- package/dist/harness/onboarding.js +30 -10
- package/dist/harness/plugins.d.ts +9 -1
- package/dist/harness/plugins.js +54 -30
- package/dist/harness/rules.js +12 -7
- package/dist/harness/sandbox.js +15 -15
- package/dist/harness/session-db.d.ts +55 -0
- package/dist/harness/session-db.js +165 -0
- package/dist/harness/session.d.ts +1 -1
- package/dist/harness/session.js +34 -15
- package/dist/harness/store.d.ts +3 -3
- package/dist/harness/store.js +6 -4
- package/dist/harness/submit-handler.d.ts +4 -4
- package/dist/harness/submit-handler.js +25 -23
- package/dist/harness/telemetry.d.ts +1 -1
- package/dist/harness/telemetry.js +23 -19
- package/dist/harness/traces.d.ts +2 -2
- package/dist/harness/traces.js +39 -33
- package/dist/harness/verification.d.ts +1 -1
- package/dist/harness/verification.js +50 -44
- package/dist/lsp/client.js +44 -40
- package/dist/main.js +98 -59
- package/dist/mcp/DeferredMcpTool.d.ts +4 -4
- package/dist/mcp/DeferredMcpTool.js +9 -5
- package/dist/mcp/McpTool.d.ts +4 -4
- package/dist/mcp/McpTool.js +8 -4
- package/dist/mcp/client.d.ts +2 -2
- package/dist/mcp/client.js +21 -21
- package/dist/mcp/loader.d.ts +1 -1
- package/dist/mcp/loader.js +17 -12
- package/dist/mcp/registry.d.ts +3 -3
- package/dist/mcp/registry.js +97 -97
- package/dist/mcp/schema.d.ts +1 -1
- package/dist/mcp/schema.js +16 -16
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +21 -21
- package/dist/mcp/types.d.ts +3 -3
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +10 -9
- package/dist/providers/base.d.ts +1 -1
- package/dist/providers/index.js +10 -3
- package/dist/providers/llamacpp.d.ts +2 -2
- package/dist/providers/llamacpp.js +1 -3
- package/dist/providers/ollama.d.ts +2 -2
- package/dist/providers/ollama.js +3 -4
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.js +3 -5
- package/dist/providers/openrouter.d.ts +2 -2
- package/dist/providers/router.d.ts +1 -1
- package/dist/providers/router.js +7 -7
- package/dist/query/compress.d.ts +2 -2
- package/dist/query/compress.js +22 -21
- package/dist/query/context-manager.d.ts +1 -1
- package/dist/query/context-manager.js +5 -5
- package/dist/query/errors.js +1 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +30 -22
- package/dist/query/tools.js +15 -12
- package/dist/query/types.d.ts +1 -1
- package/dist/query.d.ts +1 -1
- package/dist/query.js +1 -1
- package/dist/remote/auth.d.ts +2 -2
- package/dist/remote/auth.js +8 -8
- package/dist/remote/server.d.ts +3 -3
- package/dist/remote/server.js +60 -60
- package/dist/renderer/cells.js +9 -9
- package/dist/renderer/colors.js +24 -6
- package/dist/renderer/diff.d.ts +2 -2
- package/dist/renderer/diff.js +27 -19
- package/dist/renderer/differ.d.ts +1 -1
- package/dist/renderer/differ.js +9 -9
- package/dist/renderer/image.js +19 -19
- package/dist/renderer/index.d.ts +6 -6
- package/dist/renderer/index.js +163 -93
- package/dist/renderer/input.js +66 -48
- package/dist/renderer/layout.d.ts +6 -6
- package/dist/renderer/layout.js +163 -124
- package/dist/renderer/markdown.d.ts +2 -2
- package/dist/renderer/markdown.js +173 -54
- package/dist/renderer/session-browser.d.ts +2 -2
- package/dist/renderer/session-browser.js +19 -21
- package/dist/repl.d.ts +5 -5
- package/dist/repl.js +300 -198
- package/dist/sdk/index.d.ts +5 -5
- package/dist/sdk/index.js +32 -26
- package/dist/services/AgentDispatcher.d.ts +3 -3
- package/dist/services/AgentDispatcher.js +33 -29
- package/dist/services/CronExecutor.d.ts +4 -4
- package/dist/services/CronExecutor.js +12 -8
- package/dist/services/EvaluatorLoop.d.ts +3 -3
- package/dist/services/EvaluatorLoop.js +29 -21
- package/dist/services/MetaHarness.d.ts +1 -1
- package/dist/services/MetaHarness.js +34 -32
- package/dist/services/PipelineExecutor.d.ts +1 -1
- package/dist/services/PipelineExecutor.js +23 -25
- package/dist/services/SkillExtractor.d.ts +43 -0
- package/dist/services/SkillExtractor.js +143 -0
- package/dist/services/StreamingToolExecutor.d.ts +2 -2
- package/dist/services/StreamingToolExecutor.js +11 -7
- package/dist/services/a2a.d.ts +8 -8
- package/dist/services/a2a.js +44 -34
- package/dist/services/agent-messaging.d.ts +33 -15
- package/dist/services/agent-messaging.js +65 -13
- package/dist/services/cron.js +16 -16
- package/dist/tools/AgentTool/index.d.ts +5 -2
- package/dist/tools/AgentTool/index.js +35 -15
- package/dist/tools/AskUserTool/index.js +1 -1
- package/dist/tools/BashTool/index.d.ts +2 -2
- package/dist/tools/BashTool/index.js +18 -10
- package/dist/tools/CronTool/index.d.ts +2 -2
- package/dist/tools/CronTool/index.js +30 -12
- package/dist/tools/DiagnosticsTool/index.js +28 -22
- package/dist/tools/EnterPlanModeTool/index.js +93 -14
- package/dist/tools/EnterWorktreeTool/index.js +7 -3
- package/dist/tools/ExitPlanModeTool/index.d.ts +22 -1
- package/dist/tools/ExitPlanModeTool/index.js +20 -5
- package/dist/tools/ExitWorktreeTool/index.js +11 -4
- package/dist/tools/FileEditTool/index.js +3 -5
- package/dist/tools/FileReadTool/index.js +16 -10
- package/dist/tools/FileWriteTool/index.js +2 -2
- package/dist/tools/GlobTool/index.js +5 -9
- package/dist/tools/GrepTool/index.d.ts +2 -2
- package/dist/tools/GrepTool/index.js +14 -9
- package/dist/tools/ImageReadTool/index.js +2 -2
- package/dist/tools/KillProcessTool/index.js +11 -7
- package/dist/tools/LSTool/index.js +3 -3
- package/dist/tools/MemoryTool/index.d.ts +11 -11
- package/dist/tools/MemoryTool/index.js +28 -14
- package/dist/tools/MonitorTool/index.js +24 -19
- package/dist/tools/MultiEditTool/index.js +9 -5
- package/dist/tools/NotebookEditTool/index.js +3 -3
- package/dist/tools/ParallelAgentTool/index.d.ts +4 -4
- package/dist/tools/ParallelAgentTool/index.js +12 -6
- package/dist/tools/PipelineTool/index.d.ts +4 -4
- package/dist/tools/PipelineTool/index.js +3 -3
- package/dist/tools/PowerShellTool/index.js +10 -6
- package/dist/tools/RemoteTriggerTool/index.js +8 -4
- package/dist/tools/ScheduleWakeupTool/index.d.ts +42 -0
- package/dist/tools/ScheduleWakeupTool/index.js +115 -0
- package/dist/tools/SendMessageTool/index.js +25 -7
- package/dist/tools/SessionSearchTool/index.d.ts +15 -0
- package/dist/tools/SessionSearchTool/index.js +36 -0
- package/dist/tools/SkillTool/index.d.ts +3 -0
- package/dist/tools/SkillTool/index.js +39 -9
- package/dist/tools/TaskCreateTool/index.d.ts +2 -2
- package/dist/tools/TaskCreateTool/index.js +2 -2
- package/dist/tools/TaskGetTool/index.js +2 -2
- package/dist/tools/TaskListTool/index.js +3 -5
- package/dist/tools/TaskOutputTool/index.js +2 -2
- package/dist/tools/TaskStopTool/index.js +3 -3
- package/dist/tools/TaskUpdateTool/index.d.ts +4 -4
- package/dist/tools/TaskUpdateTool/index.js +2 -2
- package/dist/tools/ToolSearchTool/index.js +9 -6
- package/dist/tools/WebFetchTool/index.js +1 -1
- package/dist/tools/WebSearchTool/index.js +2 -6
- package/dist/tools.js +31 -30
- package/dist/types/permissions.js +15 -9
- package/dist/utils/bash-safety.d.ts +1 -1
- package/dist/utils/bash-safety.js +64 -54
- package/dist/utils/diff-algorithm.d.ts +3 -3
- package/dist/utils/diff-algorithm.js +7 -7
- package/dist/utils/fs.js +3 -3
- package/dist/utils/safe-env.js +1 -1
- package/dist/utils/theme-data.d.ts +1 -1
- package/dist/utils/theme-data.js +1 -1
- package/dist/utils/theme.d.ts +1 -1
- package/dist/utils/theme.js +1 -1
- package/dist/utils/tool-summary.d.ts +1 -1
- package/dist/utils/tool-summary.js +27 -9
- package/package.json +10 -3
package/dist/repl.js
CHANGED
|
@@ -2,68 +2,68 @@
|
|
|
2
2
|
* Imperative REPL — extracted business logic from React REPL.tsx.
|
|
3
3
|
* Uses TerminalRenderer for display instead of Ink.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { readOhConfig, writeOhConfig } from
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { getCommandEntries } from "./commands/index.js";
|
|
7
|
+
import { roll } from "./cybergotchi/bones.js";
|
|
8
|
+
import { loadCompanionConfig, saveCompanionConfig } from "./cybergotchi/config.js";
|
|
9
|
+
import { cybergotchiEvents } from "./cybergotchi/events.js";
|
|
10
|
+
import { getSpecies } from "./cybergotchi/species.js";
|
|
11
|
+
import { EYE_STYLES, RARITY_COLORS, RARITY_STARS } from "./cybergotchi/types.js";
|
|
12
|
+
import { autoCommitAIEdits, isGitRepo } from "./git/index.js";
|
|
13
|
+
import { readOhConfig, writeOhConfig } from "./harness/config.js";
|
|
14
|
+
import { estimateMessageTokens, getContextWarning } from "./harness/context-warning.js";
|
|
15
|
+
import { CostTracker, estimateCost, getContextWindow } from "./harness/cost.js";
|
|
16
|
+
import { createSession, loadSession, saveSession } from "./harness/session.js";
|
|
17
|
+
import { createStore } from "./harness/store.js";
|
|
18
|
+
import { handleUserInput } from "./harness/submit-handler.js";
|
|
19
|
+
import { query } from "./query/index.js";
|
|
20
|
+
import { resetDiffStyleCache } from "./renderer/diff.js";
|
|
21
|
+
import { TerminalRenderer } from "./renderer/index.js";
|
|
22
|
+
import { resetStyleCache } from "./renderer/layout.js";
|
|
23
|
+
import { resetMdStyleCache } from "./renderer/markdown.js";
|
|
24
|
+
import { createAssistantMessage, createInfoMessage, createMessage } from "./types/message.js";
|
|
25
|
+
import { formatTokenCount } from "./utils/format.js";
|
|
26
|
+
import { setActiveTheme } from "./utils/theme-data.js";
|
|
27
|
+
import { formatToolArgs, summarizeToolOutput } from "./utils/tool-summary.js";
|
|
28
28
|
export async function startREPL(config) {
|
|
29
29
|
if (config.theme)
|
|
30
30
|
setActiveTheme(config.theme);
|
|
31
31
|
const renderer = new TerminalRenderer();
|
|
32
32
|
// Set banner in live area (avoids the empty gap between scrollback banner and bottom-anchored input)
|
|
33
33
|
if (config.welcomeText) {
|
|
34
|
-
renderer.setBannerLines(config.welcomeText.split(
|
|
34
|
+
renderer.setBannerLines(config.welcomeText.split("\n"));
|
|
35
35
|
}
|
|
36
36
|
// Session
|
|
37
37
|
let session;
|
|
38
38
|
const sessionExtras = {
|
|
39
39
|
workingDir: process.cwd(),
|
|
40
|
-
gitBranch: isGitRepo() ? (await import(
|
|
41
|
-
tools: config.tools.map(t => t.name),
|
|
40
|
+
gitBranch: isGitRepo() ? (await import("./git/index.js")).gitBranch() : undefined,
|
|
41
|
+
tools: config.tools.map((t) => t.name),
|
|
42
42
|
};
|
|
43
43
|
try {
|
|
44
44
|
session = config.resumeSessionId
|
|
45
45
|
? loadSession(config.resumeSessionId)
|
|
46
|
-
: createSession(config.provider.name, config.model ??
|
|
46
|
+
: createSession(config.provider.name, config.model ?? "", sessionExtras);
|
|
47
47
|
}
|
|
48
48
|
catch {
|
|
49
|
-
session = createSession(config.provider.name, config.model ??
|
|
49
|
+
session = createSession(config.provider.name, config.model ?? "", sessionExtras);
|
|
50
50
|
}
|
|
51
51
|
// Wake context: inject session summary when resuming
|
|
52
52
|
if (config.resumeSessionId && session.hibernate) {
|
|
53
|
-
const { buildWakeContext } = await import(
|
|
53
|
+
const { buildWakeContext } = await import("./harness/session.js");
|
|
54
54
|
const wakeMsg = buildWakeContext(session);
|
|
55
|
-
const { createInfoMessage } = await import(
|
|
55
|
+
const { createInfoMessage } = await import("./types/message.js");
|
|
56
56
|
session.messages.push(createInfoMessage(wakeMsg));
|
|
57
57
|
}
|
|
58
58
|
// Initialize checkpoints for file rewind
|
|
59
|
-
const { initCheckpoints } = await import(
|
|
59
|
+
const { initCheckpoints } = await import("./harness/checkpoints.js");
|
|
60
60
|
initCheckpoints(session.id);
|
|
61
61
|
// Start background cron executor
|
|
62
|
-
const { CronExecutor } = await import(
|
|
62
|
+
const { CronExecutor } = await import("./services/CronExecutor.js");
|
|
63
63
|
const cronExecutor = new CronExecutor(config.provider, config.tools, config.systemPrompt, config.permissionMode, config.model);
|
|
64
64
|
cronExecutor.start();
|
|
65
65
|
// A2A: publish agent card for cross-process discovery
|
|
66
|
-
const { createSessionCard, publishCard, unpublishCard } = await import(
|
|
66
|
+
const { createSessionCard, publishCard, unpublishCard } = await import("./services/a2a.js");
|
|
67
67
|
const agentCard = createSessionCard(session.id, {
|
|
68
68
|
provider: config.provider.name,
|
|
69
69
|
model: config.model,
|
|
@@ -75,8 +75,8 @@ export async function startREPL(config) {
|
|
|
75
75
|
const store = createStore({
|
|
76
76
|
messages: config.resumeSessionId ? session.messages : (config.initialMessages ?? []),
|
|
77
77
|
loading: false,
|
|
78
|
-
currentModel: config.model ??
|
|
79
|
-
inputText:
|
|
78
|
+
currentModel: config.model ?? "",
|
|
79
|
+
inputText: "",
|
|
80
80
|
inputCursor: 0,
|
|
81
81
|
inputHistory: [],
|
|
82
82
|
historyIndex: -1,
|
|
@@ -127,47 +127,53 @@ export async function startREPL(config) {
|
|
|
127
127
|
});
|
|
128
128
|
function updateAutocomplete() {
|
|
129
129
|
acIsPath = false;
|
|
130
|
-
if (inputText.startsWith(
|
|
130
|
+
if (inputText.startsWith("/") && inputText.length > 1 && !inputText.includes(" ")) {
|
|
131
131
|
// Slash command autocomplete
|
|
132
132
|
const prefix = inputText.slice(1).toLowerCase();
|
|
133
|
-
const entries = getCommandEntries()
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
const entries = getCommandEntries()
|
|
134
|
+
.filter((e) => e.name.startsWith(prefix))
|
|
135
|
+
.slice(0, 5);
|
|
136
|
+
acSuggestions = entries.map((e) => e.name);
|
|
137
|
+
acDescriptions = entries.map((e) => e.description);
|
|
136
138
|
acTokenStart = 0;
|
|
137
139
|
acIndex = -1;
|
|
138
140
|
}
|
|
139
|
-
else if (inputText.length > 0 && !inputText.startsWith(
|
|
141
|
+
else if (inputText.length > 0 && !inputText.startsWith("/")) {
|
|
140
142
|
// File path autocomplete: extract token under cursor
|
|
141
143
|
const beforeCursor = inputText.slice(0, inputCursor);
|
|
142
144
|
const tokenMatch = beforeCursor.match(/(\S+)$/);
|
|
143
|
-
if (tokenMatch &&
|
|
145
|
+
if (tokenMatch &&
|
|
146
|
+
(tokenMatch[1].includes("/") ||
|
|
147
|
+
tokenMatch[1].includes("\\") ||
|
|
148
|
+
tokenMatch[1].startsWith(".") ||
|
|
149
|
+
tokenMatch[1].startsWith("~"))) {
|
|
144
150
|
const token = tokenMatch[1];
|
|
145
151
|
acTokenStart = inputCursor - token.length;
|
|
146
|
-
const expanded = token.startsWith(
|
|
147
|
-
const lastSep = Math.max(expanded.lastIndexOf(
|
|
148
|
-
const dir = lastSep >= 0 ? expanded.slice(0, lastSep + 1) :
|
|
152
|
+
const expanded = token.startsWith("~") ? token.replace("~", homedir()) : token;
|
|
153
|
+
const lastSep = Math.max(expanded.lastIndexOf("/"), expanded.lastIndexOf("\\"));
|
|
154
|
+
const dir = lastSep >= 0 ? expanded.slice(0, lastSep + 1) : ".";
|
|
149
155
|
const prefix = lastSep >= 0 ? expanded.slice(lastSep + 1) : expanded;
|
|
150
156
|
try {
|
|
151
|
-
const { readdirSync, statSync } = require(
|
|
157
|
+
const { readdirSync, statSync } = require("node:fs");
|
|
152
158
|
const entries = readdirSync(dir)
|
|
153
159
|
.filter((name) => name.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
154
160
|
.slice(0, 10);
|
|
155
161
|
acSuggestions = entries.map((name) => {
|
|
156
|
-
const full = dir ===
|
|
162
|
+
const full = dir === "." ? name : dir + name;
|
|
157
163
|
try {
|
|
158
|
-
return statSync(full).isDirectory() ? full
|
|
164
|
+
return statSync(full).isDirectory() ? `${full}/` : full;
|
|
159
165
|
}
|
|
160
166
|
catch {
|
|
161
167
|
return full;
|
|
162
168
|
}
|
|
163
169
|
});
|
|
164
170
|
acDescriptions = entries.map((name) => {
|
|
165
|
-
const full = dir ===
|
|
171
|
+
const full = dir === "." ? name : dir + name;
|
|
166
172
|
try {
|
|
167
|
-
return statSync(full).isDirectory() ?
|
|
173
|
+
return statSync(full).isDirectory() ? "[dir]" : "[file]";
|
|
168
174
|
}
|
|
169
175
|
catch {
|
|
170
|
-
return
|
|
176
|
+
return "";
|
|
171
177
|
}
|
|
172
178
|
});
|
|
173
179
|
acIsPath = acSuggestions.length > 0;
|
|
@@ -199,19 +205,19 @@ export async function startREPL(config) {
|
|
|
199
205
|
saveCompanionConfig(companionConfig);
|
|
200
206
|
const bones = roll(companionConfig.seed);
|
|
201
207
|
const species = getSpecies(bones.species);
|
|
202
|
-
const eyes = EYE_STYLES[bones.eyeStyle % EYE_STYLES.length] ??
|
|
208
|
+
const eyes = EYE_STYLES[bones.eyeStyle % EYE_STYLES.length] ?? "o o";
|
|
203
209
|
const idleFrames = species.frames.idle;
|
|
204
210
|
const color = RARITY_COLORS[bones.rarity];
|
|
205
211
|
const nameLine = `${companionConfig.soul.name} ${RARITY_STARS[bones.rarity]}`;
|
|
206
212
|
// Render initial frame
|
|
207
|
-
const frame0 = (idleFrames[0] ?? []).map((l) => l.replace(
|
|
213
|
+
const frame0 = (idleFrames[0] ?? []).map((l) => l.replace("{E}", eyes));
|
|
208
214
|
renderer.setCompanion([...frame0, nameLine], color);
|
|
209
215
|
// Animate on timer
|
|
210
216
|
renderer.onAnimation((frameIdx) => {
|
|
211
217
|
if (!companionVisible)
|
|
212
218
|
return;
|
|
213
219
|
const f = idleFrames[frameIdx % idleFrames.length] ?? idleFrames[0] ?? [];
|
|
214
|
-
const lines = f.map((l) => l.replace(
|
|
220
|
+
const lines = f.map((l) => l.replace("{E}", eyes));
|
|
215
221
|
renderer.setCompanion([...lines, nameLine], color);
|
|
216
222
|
});
|
|
217
223
|
}
|
|
@@ -219,42 +225,54 @@ export async function startREPL(config) {
|
|
|
219
225
|
/** Sync local aliases back to the centralized store */
|
|
220
226
|
function syncStore() {
|
|
221
227
|
store.setState({
|
|
222
|
-
messages,
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
messages,
|
|
229
|
+
loading,
|
|
230
|
+
currentModel,
|
|
231
|
+
inputText,
|
|
232
|
+
inputCursor,
|
|
233
|
+
inputHistory,
|
|
234
|
+
historyIndex,
|
|
235
|
+
vimMode,
|
|
236
|
+
fastMode,
|
|
237
|
+
acSuggestions,
|
|
238
|
+
acDescriptions,
|
|
239
|
+
acIndex,
|
|
240
|
+
acTokenStart,
|
|
241
|
+
acIsPath,
|
|
225
242
|
});
|
|
226
243
|
}
|
|
227
244
|
function syncRenderer() {
|
|
228
245
|
syncStore();
|
|
229
246
|
renderer.setMessages(messages);
|
|
230
247
|
renderer.setLoading(loading);
|
|
231
|
-
const hints = `exit to quit${loading ?
|
|
248
|
+
const hints = `exit to quit${loading ? " | Ctrl+C stop | Ctrl+O thinking" : " | Tab expand tools | Ctrl+O transcript"}${companionConfig?.soul?.name ? ` | @${companionConfig.soul.name}` : ""}`;
|
|
232
249
|
renderer.setStatusHints(hints);
|
|
233
250
|
// Status line: model | tokens | cost | ctx
|
|
234
251
|
const inTok = cost.totalInputTokens;
|
|
235
252
|
const outTok = cost.totalOutputTokens;
|
|
236
253
|
const totalCostVal = cost.totalCost;
|
|
237
|
-
const tokensStr =
|
|
238
|
-
const costStr = totalCostVal > 0 ? `$${totalCostVal.toFixed(4)}` :
|
|
239
|
-
let ctxStr =
|
|
254
|
+
const tokensStr = inTok > 0 || outTok > 0 ? `${formatTokenCount(inTok)}↑ ${formatTokenCount(outTok)}↓` : "";
|
|
255
|
+
const costStr = totalCostVal > 0 ? `$${totalCostVal.toFixed(4)}` : "";
|
|
256
|
+
let ctxStr = "";
|
|
240
257
|
const ctxWindow = getContextWindow(currentModel);
|
|
241
258
|
if (ctxWindow > 0 && estimatedTokenCount > 0) {
|
|
242
259
|
const usage = Math.min(1, estimatedTokenCount / ctxWindow);
|
|
243
260
|
const barWidth = 10;
|
|
244
261
|
const filled = Math.max(1, Math.round(usage * barWidth));
|
|
245
|
-
const bar =
|
|
262
|
+
const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
|
|
246
263
|
const pct = Math.max(1, Math.ceil(usage * 100));
|
|
247
264
|
ctxStr = `ctx [${bar}] ${pct}%`;
|
|
248
265
|
}
|
|
249
266
|
// Use template if configured, otherwise default format
|
|
250
267
|
if (cachedConfig?.statusLineFormat) {
|
|
251
268
|
const line = cachedConfig.statusLineFormat
|
|
252
|
-
.replace(
|
|
253
|
-
.replace(
|
|
254
|
-
.replace(
|
|
255
|
-
.replace(
|
|
256
|
-
.replace(/\s*│\s*│/g,
|
|
257
|
-
.replace(/^│\s*/,
|
|
269
|
+
.replace("{model}", currentModel || "")
|
|
270
|
+
.replace("{tokens}", tokensStr)
|
|
271
|
+
.replace("{cost}", costStr)
|
|
272
|
+
.replace("{ctx}", ctxStr)
|
|
273
|
+
.replace(/\s*│\s*│/g, "│") // collapse empty sections
|
|
274
|
+
.replace(/^│\s*/, "")
|
|
275
|
+
.replace(/\s*│$/, ""); // trim leading/trailing separators
|
|
258
276
|
renderer.setStatusLine(line);
|
|
259
277
|
}
|
|
260
278
|
else {
|
|
@@ -267,7 +285,7 @@ export async function startREPL(config) {
|
|
|
267
285
|
parts.push(costStr);
|
|
268
286
|
if (ctxStr)
|
|
269
287
|
parts.push(ctxStr);
|
|
270
|
-
renderer.setStatusLine(parts.join(
|
|
288
|
+
renderer.setStatusLine(parts.join(" │ "));
|
|
271
289
|
}
|
|
272
290
|
// Context warning
|
|
273
291
|
updateContextWarning();
|
|
@@ -284,7 +302,7 @@ export async function startREPL(config) {
|
|
|
284
302
|
// Input handling
|
|
285
303
|
renderer.onKeypress((key) => {
|
|
286
304
|
// Ctrl+C: abort or exit
|
|
287
|
-
if (key.ctrl && key.char ===
|
|
305
|
+
if (key.ctrl && key.char === "c") {
|
|
288
306
|
if (loading && abortController) {
|
|
289
307
|
abortController.abort();
|
|
290
308
|
}
|
|
@@ -297,84 +315,84 @@ export async function startREPL(config) {
|
|
|
297
315
|
// Search: use terminal's native search (Ctrl+Shift+F in VS Code)
|
|
298
316
|
// Vim mode
|
|
299
317
|
if (vimMode !== null) {
|
|
300
|
-
if (key.name ===
|
|
301
|
-
vimMode =
|
|
318
|
+
if (key.name === "escape") {
|
|
319
|
+
vimMode = "normal";
|
|
302
320
|
renderer.setVimMode(vimMode);
|
|
303
321
|
return;
|
|
304
322
|
}
|
|
305
|
-
if (vimMode ===
|
|
323
|
+
if (vimMode === "normal") {
|
|
306
324
|
// -- Mode transitions --
|
|
307
|
-
if (key.char ===
|
|
308
|
-
vimMode =
|
|
325
|
+
if (key.char === "i") {
|
|
326
|
+
vimMode = "insert";
|
|
309
327
|
renderer.setVimMode(vimMode);
|
|
310
328
|
return;
|
|
311
329
|
}
|
|
312
|
-
if (key.char ===
|
|
313
|
-
vimMode =
|
|
330
|
+
if (key.char === "a") {
|
|
331
|
+
vimMode = "insert";
|
|
314
332
|
renderer.setVimMode(vimMode);
|
|
315
333
|
if (inputCursor < inputText.length)
|
|
316
334
|
inputCursor++;
|
|
317
335
|
renderer.setInputCursor(inputCursor);
|
|
318
336
|
return;
|
|
319
337
|
}
|
|
320
|
-
if (key.char ===
|
|
321
|
-
vimMode =
|
|
338
|
+
if (key.char === "I") {
|
|
339
|
+
vimMode = "insert";
|
|
322
340
|
renderer.setVimMode(vimMode);
|
|
323
341
|
inputCursor = 0;
|
|
324
342
|
renderer.setInputCursor(inputCursor);
|
|
325
343
|
return;
|
|
326
344
|
}
|
|
327
|
-
if (key.char ===
|
|
328
|
-
vimMode =
|
|
345
|
+
if (key.char === "A") {
|
|
346
|
+
vimMode = "insert";
|
|
329
347
|
renderer.setVimMode(vimMode);
|
|
330
348
|
inputCursor = inputText.length;
|
|
331
349
|
renderer.setInputCursor(inputCursor);
|
|
332
350
|
return;
|
|
333
351
|
}
|
|
334
|
-
if (key.char ===
|
|
335
|
-
vimMode =
|
|
352
|
+
if (key.char === "o") {
|
|
353
|
+
vimMode = "insert";
|
|
336
354
|
renderer.setVimMode(vimMode);
|
|
337
|
-
inputText = inputText
|
|
355
|
+
inputText = `${inputText}\n`;
|
|
338
356
|
inputCursor = inputText.length;
|
|
339
357
|
renderer.setInputText(inputText);
|
|
340
358
|
renderer.setInputCursor(inputCursor);
|
|
341
359
|
return;
|
|
342
360
|
}
|
|
343
361
|
// -- Movement --
|
|
344
|
-
if (key.char ===
|
|
362
|
+
if (key.char === "h" || key.name === "left") {
|
|
345
363
|
if (inputCursor > 0) {
|
|
346
364
|
inputCursor--;
|
|
347
365
|
renderer.setInputCursor(inputCursor);
|
|
348
366
|
}
|
|
349
367
|
return;
|
|
350
368
|
}
|
|
351
|
-
if (key.char ===
|
|
369
|
+
if (key.char === "l" || key.name === "right") {
|
|
352
370
|
if (inputCursor < inputText.length) {
|
|
353
371
|
inputCursor++;
|
|
354
372
|
renderer.setInputCursor(inputCursor);
|
|
355
373
|
}
|
|
356
374
|
return;
|
|
357
375
|
}
|
|
358
|
-
if (key.char ===
|
|
376
|
+
if (key.char === "j" || key.name === "down") {
|
|
359
377
|
navigateHistory(1);
|
|
360
378
|
return;
|
|
361
379
|
}
|
|
362
|
-
if (key.char ===
|
|
380
|
+
if (key.char === "k" || key.name === "up") {
|
|
363
381
|
navigateHistory(-1);
|
|
364
382
|
return;
|
|
365
383
|
}
|
|
366
|
-
if (key.char ===
|
|
384
|
+
if (key.char === "0") {
|
|
367
385
|
inputCursor = 0;
|
|
368
386
|
renderer.setInputCursor(inputCursor);
|
|
369
387
|
return;
|
|
370
388
|
}
|
|
371
|
-
if (key.char ===
|
|
389
|
+
if (key.char === "$") {
|
|
372
390
|
inputCursor = inputText.length;
|
|
373
391
|
renderer.setInputCursor(inputCursor);
|
|
374
392
|
return;
|
|
375
393
|
}
|
|
376
394
|
// Word forward (w)
|
|
377
|
-
if (key.char ===
|
|
395
|
+
if (key.char === "w") {
|
|
378
396
|
const rest = inputText.slice(inputCursor);
|
|
379
397
|
const m = rest.match(/^\S*\s+/);
|
|
380
398
|
inputCursor = m ? Math.min(inputCursor + m[0].length, inputText.length) : inputText.length;
|
|
@@ -382,7 +400,7 @@ export async function startREPL(config) {
|
|
|
382
400
|
return;
|
|
383
401
|
}
|
|
384
402
|
// Word backward (b)
|
|
385
|
-
if (key.char ===
|
|
403
|
+
if (key.char === "b") {
|
|
386
404
|
const before = inputText.slice(0, inputCursor);
|
|
387
405
|
const m = before.match(/\S+\s*$/);
|
|
388
406
|
inputCursor = m ? inputCursor - m[0].length : 0;
|
|
@@ -390,7 +408,7 @@ export async function startREPL(config) {
|
|
|
390
408
|
return;
|
|
391
409
|
}
|
|
392
410
|
// End of word (e)
|
|
393
|
-
if (key.char ===
|
|
411
|
+
if (key.char === "e") {
|
|
394
412
|
const rest = inputText.slice(inputCursor + 1);
|
|
395
413
|
const m = rest.match(/^\s*\S*/);
|
|
396
414
|
inputCursor = m ? Math.min(inputCursor + 1 + m[0].length, inputText.length) : inputText.length;
|
|
@@ -398,7 +416,7 @@ export async function startREPL(config) {
|
|
|
398
416
|
return;
|
|
399
417
|
}
|
|
400
418
|
// -- Editing --
|
|
401
|
-
if (key.char ===
|
|
419
|
+
if (key.char === "x") {
|
|
402
420
|
if (inputCursor < inputText.length) {
|
|
403
421
|
inputText = inputText.slice(0, inputCursor) + inputText.slice(inputCursor + 1);
|
|
404
422
|
if (inputCursor >= inputText.length && inputCursor > 0)
|
|
@@ -409,21 +427,21 @@ export async function startREPL(config) {
|
|
|
409
427
|
return;
|
|
410
428
|
}
|
|
411
429
|
// dd — delete entire line
|
|
412
|
-
if (key.char ===
|
|
430
|
+
if (key.char === "d") {
|
|
413
431
|
// Simple: clear entire input (like dd in single-line mode)
|
|
414
|
-
inputText =
|
|
432
|
+
inputText = "";
|
|
415
433
|
inputCursor = 0;
|
|
416
434
|
renderer.setInputText(inputText);
|
|
417
435
|
renderer.setInputCursor(inputCursor);
|
|
418
436
|
return;
|
|
419
437
|
}
|
|
420
438
|
// Submit with Enter even in normal mode
|
|
421
|
-
if (key.name ===
|
|
439
|
+
if (key.name === "return") {
|
|
422
440
|
if (inputText.trim() && !loading) {
|
|
423
441
|
handleSubmit(inputText.trim());
|
|
424
442
|
inputHistory.unshift(inputText);
|
|
425
443
|
historyIndex = -1;
|
|
426
|
-
inputText =
|
|
444
|
+
inputText = "";
|
|
427
445
|
inputCursor = 0;
|
|
428
446
|
acSuggestions = [];
|
|
429
447
|
acDescriptions = [];
|
|
@@ -438,25 +456,25 @@ export async function startREPL(config) {
|
|
|
438
456
|
}
|
|
439
457
|
// Session browser navigation
|
|
440
458
|
if (renderer.isSessionBrowserOpen()) {
|
|
441
|
-
if (key.name ===
|
|
459
|
+
if (key.name === "up") {
|
|
442
460
|
renderer.sessionBrowserUp();
|
|
443
461
|
return;
|
|
444
462
|
}
|
|
445
|
-
if (key.name ===
|
|
463
|
+
if (key.name === "down") {
|
|
446
464
|
renderer.sessionBrowserDown();
|
|
447
465
|
return;
|
|
448
466
|
}
|
|
449
|
-
if (key.name ===
|
|
467
|
+
if (key.name === "return") {
|
|
450
468
|
const id = renderer.sessionBrowserSelect();
|
|
451
469
|
if (id)
|
|
452
470
|
handleSubmit(`/resume ${id}`);
|
|
453
471
|
return;
|
|
454
472
|
}
|
|
455
|
-
if (key.name ===
|
|
473
|
+
if (key.name === "escape") {
|
|
456
474
|
renderer.closeSessionBrowser();
|
|
457
475
|
return;
|
|
458
476
|
}
|
|
459
|
-
if (key.name ===
|
|
477
|
+
if (key.name === "backspace") {
|
|
460
478
|
renderer.sessionBrowserBackspace();
|
|
461
479
|
return;
|
|
462
480
|
}
|
|
@@ -467,12 +485,12 @@ export async function startREPL(config) {
|
|
|
467
485
|
return; // swallow other keys during browser
|
|
468
486
|
}
|
|
469
487
|
// Ctrl+K: toggle code block expansion
|
|
470
|
-
if (key.ctrl && key.char ===
|
|
488
|
+
if (key.ctrl && key.char === "k" && !loading) {
|
|
471
489
|
renderer.toggleCodeBlockExpansion();
|
|
472
490
|
return;
|
|
473
491
|
}
|
|
474
492
|
// Ctrl+O: cycle through views — thinking toggle → transcript (flush all to scrollback)
|
|
475
|
-
if (key.ctrl && key.char ===
|
|
493
|
+
if (key.ctrl && key.char === "o") {
|
|
476
494
|
if (loading) {
|
|
477
495
|
// During streaming: toggle thinking expansion
|
|
478
496
|
renderer.toggleThinkingExpanded();
|
|
@@ -483,23 +501,23 @@ export async function startREPL(config) {
|
|
|
483
501
|
renderer.clearLiveArea();
|
|
484
502
|
renderer.setMessages(messages);
|
|
485
503
|
renderer.flushMessages();
|
|
486
|
-
renderer.notify(
|
|
504
|
+
renderer.notify("Transcript written to scrollback (scroll up to review)");
|
|
487
505
|
}
|
|
488
506
|
return;
|
|
489
507
|
}
|
|
490
508
|
// Scroll wheel: adjust manual scroll offset
|
|
491
|
-
if (key.name ===
|
|
509
|
+
if (key.name === "scrollup") {
|
|
492
510
|
renderer.scrollBy(3);
|
|
493
511
|
return;
|
|
494
512
|
}
|
|
495
|
-
if (key.name ===
|
|
513
|
+
if (key.name === "scrolldown") {
|
|
496
514
|
renderer.scrollBy(-3);
|
|
497
515
|
return;
|
|
498
516
|
}
|
|
499
|
-
if (key.name ===
|
|
517
|
+
if (key.name === "pageup" || key.name === "pagedown" || key.name === "mouse")
|
|
500
518
|
return;
|
|
501
519
|
// Tab: autocomplete slash commands or file paths, or cycle tool call expansion
|
|
502
|
-
if (key.name ===
|
|
520
|
+
if (key.name === "tab" && !loading) {
|
|
503
521
|
if (acSuggestions.length > 0) {
|
|
504
522
|
acIndex = (acIndex + 1) % acSuggestions.length;
|
|
505
523
|
if (acIsPath) {
|
|
@@ -522,20 +540,20 @@ export async function startREPL(config) {
|
|
|
522
540
|
return;
|
|
523
541
|
}
|
|
524
542
|
// Alt+Enter or paste newline: insert newline at cursor
|
|
525
|
-
if (key.name ===
|
|
526
|
-
inputText = inputText.slice(0, inputCursor)
|
|
543
|
+
if (key.name === "newline") {
|
|
544
|
+
inputText = `${inputText.slice(0, inputCursor)}\n${inputText.slice(inputCursor)}`;
|
|
527
545
|
inputCursor++;
|
|
528
546
|
renderer.setInputText(inputText);
|
|
529
547
|
renderer.setInputCursor(inputCursor);
|
|
530
548
|
return;
|
|
531
549
|
}
|
|
532
550
|
// Enter: submit
|
|
533
|
-
if (key.name ===
|
|
551
|
+
if (key.name === "return") {
|
|
534
552
|
if (inputText.trim() && !loading) {
|
|
535
553
|
handleSubmit(inputText.trim());
|
|
536
554
|
inputHistory.unshift(inputText);
|
|
537
555
|
historyIndex = -1;
|
|
538
|
-
inputText =
|
|
556
|
+
inputText = "";
|
|
539
557
|
inputCursor = 0;
|
|
540
558
|
acSuggestions = [];
|
|
541
559
|
acIndex = -1;
|
|
@@ -546,36 +564,36 @@ export async function startREPL(config) {
|
|
|
546
564
|
return;
|
|
547
565
|
}
|
|
548
566
|
// History
|
|
549
|
-
if (key.name ===
|
|
567
|
+
if (key.name === "up") {
|
|
550
568
|
navigateHistory(-1);
|
|
551
569
|
return;
|
|
552
570
|
}
|
|
553
|
-
if (key.name ===
|
|
571
|
+
if (key.name === "down") {
|
|
554
572
|
navigateHistory(1);
|
|
555
573
|
return;
|
|
556
574
|
}
|
|
557
575
|
// Editing
|
|
558
|
-
if (key.name ===
|
|
576
|
+
if (key.name === "backspace") {
|
|
559
577
|
if (inputCursor > 0) {
|
|
560
578
|
inputText = inputText.slice(0, inputCursor - 1) + inputText.slice(inputCursor);
|
|
561
579
|
inputCursor--;
|
|
562
580
|
}
|
|
563
581
|
}
|
|
564
|
-
else if (key.name ===
|
|
582
|
+
else if (key.name === "delete") {
|
|
565
583
|
inputText = inputText.slice(0, inputCursor) + inputText.slice(inputCursor + 1);
|
|
566
584
|
}
|
|
567
|
-
else if (key.name ===
|
|
585
|
+
else if (key.name === "left") {
|
|
568
586
|
if (inputCursor > 0)
|
|
569
587
|
inputCursor--;
|
|
570
588
|
}
|
|
571
|
-
else if (key.name ===
|
|
589
|
+
else if (key.name === "right") {
|
|
572
590
|
if (inputCursor < inputText.length)
|
|
573
591
|
inputCursor++;
|
|
574
592
|
}
|
|
575
|
-
else if (key.ctrl && key.char ===
|
|
593
|
+
else if (key.ctrl && key.char === "a") {
|
|
576
594
|
inputCursor = 0;
|
|
577
595
|
}
|
|
578
|
-
else if (key.ctrl && key.char ===
|
|
596
|
+
else if (key.ctrl && key.char === "e") {
|
|
579
597
|
inputCursor = inputText.length;
|
|
580
598
|
}
|
|
581
599
|
else if (key.char && key.char.length === 1 && !key.ctrl && !key.meta) {
|
|
@@ -587,9 +605,20 @@ export async function startREPL(config) {
|
|
|
587
605
|
updateAutocomplete();
|
|
588
606
|
// Sync local aliases back to store after each keypress
|
|
589
607
|
store.setState({
|
|
590
|
-
messages,
|
|
591
|
-
|
|
592
|
-
|
|
608
|
+
messages,
|
|
609
|
+
loading,
|
|
610
|
+
currentModel,
|
|
611
|
+
inputText,
|
|
612
|
+
inputCursor,
|
|
613
|
+
inputHistory,
|
|
614
|
+
historyIndex,
|
|
615
|
+
vimMode,
|
|
616
|
+
fastMode,
|
|
617
|
+
acSuggestions,
|
|
618
|
+
acDescriptions,
|
|
619
|
+
acIndex,
|
|
620
|
+
acTokenStart,
|
|
621
|
+
acIsPath,
|
|
593
622
|
});
|
|
594
623
|
});
|
|
595
624
|
function navigateHistory(dir) {
|
|
@@ -600,7 +629,7 @@ export async function startREPL(config) {
|
|
|
600
629
|
else if (dir > 0) {
|
|
601
630
|
if (historyIndex <= 0) {
|
|
602
631
|
historyIndex = -1;
|
|
603
|
-
inputText =
|
|
632
|
+
inputText = "";
|
|
604
633
|
}
|
|
605
634
|
else {
|
|
606
635
|
historyIndex--;
|
|
@@ -615,29 +644,65 @@ export async function startREPL(config) {
|
|
|
615
644
|
// Clear any previous errors on new input
|
|
616
645
|
renderer.setError(null);
|
|
617
646
|
// Exit
|
|
618
|
-
if (input ===
|
|
647
|
+
if (input === "exit" || input === "quit" || input === "/exit" || input === "/quit" || input === "/q") {
|
|
619
648
|
// Hibernate: save session state for potential wake-up resume
|
|
620
649
|
try {
|
|
621
|
-
const { buildHibernateState } = await import(
|
|
650
|
+
const { buildHibernateState } = await import("./harness/session.js");
|
|
622
651
|
session.hibernate = buildHibernateState(messages);
|
|
623
652
|
}
|
|
624
|
-
catch {
|
|
653
|
+
catch {
|
|
654
|
+
/* ignore */
|
|
655
|
+
}
|
|
625
656
|
// Dream consolidation: prune stale memories before exit
|
|
626
657
|
try {
|
|
627
|
-
const { consolidateMemories } = await import(
|
|
628
|
-
const { readOhConfig } = await import(
|
|
658
|
+
const { consolidateMemories } = await import("./harness/memory.js");
|
|
659
|
+
const { readOhConfig } = await import("./harness/config.js");
|
|
629
660
|
const ohCfg = readOhConfig();
|
|
630
661
|
if (ohCfg?.memory?.consolidateOnExit !== false) {
|
|
631
662
|
consolidateMemories();
|
|
632
663
|
}
|
|
633
664
|
}
|
|
634
|
-
catch {
|
|
665
|
+
catch {
|
|
666
|
+
/* ignore */
|
|
667
|
+
}
|
|
668
|
+
// Post-session learning: extract skills + update user profile
|
|
669
|
+
try {
|
|
670
|
+
const { runExtraction } = await import("./services/SkillExtractor.js");
|
|
671
|
+
const { updateUserProfile, loadUserProfile, detectMemories } = await import("./harness/memory.js");
|
|
672
|
+
// Skill extraction (async, may take a few seconds)
|
|
673
|
+
const extracted = await runExtraction(config.provider, messages, session.id, currentModel);
|
|
674
|
+
if (extracted.length > 0) {
|
|
675
|
+
console.log(`[learn] Extracted ${extracted.length} skill(s) from this session.`);
|
|
676
|
+
}
|
|
677
|
+
// User profile update
|
|
678
|
+
if (messages.length >= 6) {
|
|
679
|
+
const detected = await detectMemories(config.provider, messages, currentModel);
|
|
680
|
+
const profileUpdates = detected.filter((d) => d.type === "user");
|
|
681
|
+
if (profileUpdates.length > 0) {
|
|
682
|
+
const currentProfile = loadUserProfile();
|
|
683
|
+
const newObservations = profileUpdates.map((d) => d.content).join("\n");
|
|
684
|
+
const merged = currentProfile
|
|
685
|
+
? `${currentProfile}\n\n## Recent Observations\n${newObservations}`
|
|
686
|
+
: newObservations;
|
|
687
|
+
updateUserProfile(merged);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
/* learning is optional — don't block exit */
|
|
693
|
+
}
|
|
635
694
|
// Emit sessionEnd hook
|
|
636
695
|
try {
|
|
637
|
-
const { emitHookAsync } = await import(
|
|
638
|
-
await emitHookAsync(
|
|
696
|
+
const { emitHookAsync } = await import("./harness/hooks.js");
|
|
697
|
+
await emitHookAsync("sessionEnd", {
|
|
698
|
+
sessionId: session.id,
|
|
699
|
+
model: currentModel,
|
|
700
|
+
provider: config.provider.name,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
/* ignore */
|
|
639
705
|
}
|
|
640
|
-
catch { /* ignore */ }
|
|
641
706
|
cleanup();
|
|
642
707
|
process.exit(0);
|
|
643
708
|
}
|
|
@@ -653,14 +718,14 @@ export async function startREPL(config) {
|
|
|
653
718
|
messages = result.messages;
|
|
654
719
|
// Check for special commands
|
|
655
720
|
const lastMsg = messages[messages.length - 1];
|
|
656
|
-
if (lastMsg?.content ===
|
|
721
|
+
if (lastMsg?.content === "__OPEN_SESSION_BROWSER__") {
|
|
657
722
|
messages = messages.slice(0, -1);
|
|
658
723
|
renderer.openSessionBrowser();
|
|
659
724
|
syncRenderer();
|
|
660
725
|
return;
|
|
661
726
|
}
|
|
662
|
-
if (lastMsg?.content?.startsWith(
|
|
663
|
-
const themeName = lastMsg.content.split(
|
|
727
|
+
if (lastMsg?.content?.startsWith("__SWITCH_THEME__:")) {
|
|
728
|
+
const themeName = lastMsg.content.split(":")[1];
|
|
664
729
|
messages = messages.slice(0, -1);
|
|
665
730
|
setActiveTheme(themeName);
|
|
666
731
|
resetStyleCache();
|
|
@@ -668,35 +733,41 @@ export async function startREPL(config) {
|
|
|
668
733
|
resetDiffStyleCache();
|
|
669
734
|
// Persist theme to config
|
|
670
735
|
try {
|
|
671
|
-
const cfg = cachedConfig ?? {
|
|
736
|
+
const cfg = cachedConfig ?? {
|
|
737
|
+
provider: config.provider.name,
|
|
738
|
+
model: currentModel,
|
|
739
|
+
permissionMode: config.permissionMode,
|
|
740
|
+
};
|
|
672
741
|
cfg.theme = themeName;
|
|
673
742
|
writeOhConfig(cfg);
|
|
674
743
|
cachedConfig = cfg;
|
|
675
744
|
}
|
|
676
|
-
catch {
|
|
745
|
+
catch {
|
|
746
|
+
/* ignore */
|
|
747
|
+
}
|
|
677
748
|
messages = [...messages, createInfoMessage(`Theme switched to ${themeName}`)];
|
|
678
749
|
syncRenderer();
|
|
679
750
|
return;
|
|
680
751
|
}
|
|
681
|
-
if (lastMsg?.content ===
|
|
682
|
-
companionVisible = lastMsg.content ===
|
|
752
|
+
if (lastMsg?.content === "__COMPANION_OFF__" || lastMsg?.content === "__COMPANION_ON__") {
|
|
753
|
+
companionVisible = lastMsg.content === "__COMPANION_ON__";
|
|
683
754
|
messages = messages.slice(0, -1);
|
|
684
755
|
if (!companionVisible)
|
|
685
|
-
renderer.setCompanion(null,
|
|
686
|
-
messages = [...messages, createInfoMessage(`Companion ${companionVisible ?
|
|
756
|
+
renderer.setCompanion(null, "cyan");
|
|
757
|
+
messages = [...messages, createInfoMessage(`Companion ${companionVisible ? "shown" : "hidden"}`)];
|
|
687
758
|
syncRenderer();
|
|
688
759
|
return;
|
|
689
760
|
}
|
|
690
761
|
if (result.newModel)
|
|
691
762
|
currentModel = result.newModel;
|
|
692
763
|
if (result.vimToggled) {
|
|
693
|
-
vimMode = vimMode === null ?
|
|
694
|
-
messages = [...messages, createInfoMessage(vimMode ?
|
|
764
|
+
vimMode = vimMode === null ? "normal" : null;
|
|
765
|
+
messages = [...messages, createInfoMessage(vimMode ? "Vim mode ON" : "Vim mode OFF")];
|
|
695
766
|
renderer.setVimMode(vimMode);
|
|
696
767
|
}
|
|
697
768
|
if (result.fastModeToggled) {
|
|
698
769
|
fastMode = !fastMode;
|
|
699
|
-
messages = [...messages, createInfoMessage(fastMode ?
|
|
770
|
+
messages = [...messages, createInfoMessage(fastMode ? "Fast mode ON — optimized for speed" : "Fast mode OFF")];
|
|
700
771
|
}
|
|
701
772
|
// Clear old live area BEFORE syncRenderer when a query will follow.
|
|
702
773
|
// syncRenderer → scheduleRender → queueMicrotask(render). The microtask fires
|
|
@@ -721,16 +792,17 @@ export async function startREPL(config) {
|
|
|
721
792
|
renderer.setError(null);
|
|
722
793
|
renderer.clearToolCalls();
|
|
723
794
|
abortController = new AbortController();
|
|
724
|
-
let accumulated =
|
|
795
|
+
let accumulated = "";
|
|
725
796
|
const callIdToToolName = new Map();
|
|
726
797
|
const askUser = (toolName, description, riskLevel) => {
|
|
727
|
-
return renderer.askPermission(toolName, description, riskLevel ??
|
|
798
|
+
return renderer.askPermission(toolName, description, riskLevel ?? "medium");
|
|
728
799
|
};
|
|
729
800
|
const askUserQuestion = (question, options) => {
|
|
730
801
|
return renderer.askQuestion(question, options);
|
|
731
802
|
};
|
|
732
803
|
const effectiveSystemPrompt = fastMode
|
|
733
|
-
? config.systemPrompt +
|
|
804
|
+
? config.systemPrompt +
|
|
805
|
+
"\n\nIMPORTANT: Fast mode is active. Be extremely concise. Skip explanations. Go straight to the answer or action."
|
|
734
806
|
: config.systemPrompt;
|
|
735
807
|
const queryConfig = {
|
|
736
808
|
provider: config.provider,
|
|
@@ -745,116 +817,135 @@ export async function startREPL(config) {
|
|
|
745
817
|
try {
|
|
746
818
|
for await (const event of query(prompt, queryConfig, messages)) {
|
|
747
819
|
switch (event.type) {
|
|
748
|
-
case
|
|
820
|
+
case "text_delta": {
|
|
749
821
|
// Content auto-scrolls via terminal native scrollback
|
|
750
822
|
accumulated += event.content;
|
|
751
823
|
// Move completed lines to messages, keep partial in streaming
|
|
752
|
-
const lines = accumulated.split(
|
|
824
|
+
const lines = accumulated.split("\n");
|
|
753
825
|
if (lines.length > 1) {
|
|
754
|
-
const completedText = lines.slice(0, -1).join(
|
|
826
|
+
const completedText = lines.slice(0, -1).join("\n");
|
|
755
827
|
const last = messages[messages.length - 1];
|
|
756
828
|
if (last?.meta?.isStreaming) {
|
|
757
|
-
messages = [...messages.slice(0, -1), { ...last, content: last.content + completedText
|
|
829
|
+
messages = [...messages.slice(0, -1), { ...last, content: `${last.content + completedText}\n` }];
|
|
758
830
|
}
|
|
759
831
|
else {
|
|
760
|
-
messages = [
|
|
832
|
+
messages = [
|
|
833
|
+
...messages,
|
|
834
|
+
createMessage("assistant", `${completedText}\n`, { meta: { isStreaming: true } }),
|
|
835
|
+
];
|
|
761
836
|
}
|
|
762
837
|
accumulated = lines[lines.length - 1];
|
|
763
838
|
}
|
|
764
839
|
renderer.setMessages(messages);
|
|
765
840
|
renderer.setStreamingText(accumulated);
|
|
766
841
|
break;
|
|
767
|
-
|
|
842
|
+
}
|
|
843
|
+
case "thinking_delta":
|
|
768
844
|
if (!renderer.getThinkingStartedAt())
|
|
769
845
|
renderer.setThinkingStartedAt(Date.now());
|
|
770
846
|
renderer.setThinkingText(event.content);
|
|
771
847
|
break;
|
|
772
|
-
case
|
|
848
|
+
case "tool_call_start": {
|
|
773
849
|
callIdToToolName.set(event.callId, event.toolName);
|
|
774
|
-
const isAgentTool = event.toolName ===
|
|
775
|
-
renderer.setToolCall(event.callId, {
|
|
850
|
+
const isAgentTool = event.toolName === "Agent" || event.toolName === "ParallelAgents";
|
|
851
|
+
renderer.setToolCall(event.callId, {
|
|
852
|
+
toolName: event.toolName,
|
|
853
|
+
status: "running",
|
|
854
|
+
startedAt: Date.now(),
|
|
855
|
+
isAgent: isAgentTool,
|
|
856
|
+
});
|
|
776
857
|
break;
|
|
777
858
|
}
|
|
778
|
-
case
|
|
779
|
-
const tcToolName = callIdToToolName.get(event.callId) ??
|
|
859
|
+
case "tool_call_complete": {
|
|
860
|
+
const tcToolName = callIdToToolName.get(event.callId) ?? "";
|
|
780
861
|
const existingTc = renderer.getToolCall(event.callId);
|
|
781
|
-
const isAgentCall = tcToolName ===
|
|
782
|
-
const agentDesc = isAgentCall
|
|
862
|
+
const isAgentCall = tcToolName === "Agent" || tcToolName === "ParallelAgents";
|
|
863
|
+
const agentDesc = isAgentCall
|
|
864
|
+
? event.arguments.description
|
|
865
|
+
: undefined;
|
|
783
866
|
renderer.setToolCall(event.callId, {
|
|
784
867
|
...existingTc,
|
|
785
868
|
toolName: tcToolName,
|
|
786
|
-
status:
|
|
869
|
+
status: "running",
|
|
787
870
|
args: formatToolArgs(tcToolName, event.arguments),
|
|
788
871
|
agentDescription: agentDesc ?? existingTc?.agentDescription,
|
|
789
872
|
});
|
|
790
873
|
break;
|
|
791
874
|
}
|
|
792
|
-
case
|
|
875
|
+
case "tool_output_delta": {
|
|
793
876
|
// Accumulate streaming output lines
|
|
794
877
|
const existing = renderer.getToolCall(event.callId) ?? {
|
|
795
|
-
toolName: callIdToToolName.get(event.callId) ??
|
|
796
|
-
status:
|
|
878
|
+
toolName: callIdToToolName.get(event.callId) ?? "unknown",
|
|
879
|
+
status: "running",
|
|
797
880
|
};
|
|
798
881
|
const lines = existing.liveOutput ?? [];
|
|
799
|
-
const chunks = event.chunk.split(
|
|
882
|
+
const chunks = event.chunk.split("\n");
|
|
800
883
|
const merged = [...lines];
|
|
801
|
-
if (merged.length > 0 && !event.chunk.startsWith(
|
|
802
|
-
merged[merged.length - 1] = (merged[merged.length - 1] ??
|
|
803
|
-
merged.push(...chunks.slice(1).filter((c) => c !==
|
|
884
|
+
if (merged.length > 0 && !event.chunk.startsWith("\n")) {
|
|
885
|
+
merged[merged.length - 1] = (merged[merged.length - 1] ?? "") + chunks[0];
|
|
886
|
+
merged.push(...chunks.slice(1).filter((c) => c !== ""));
|
|
804
887
|
}
|
|
805
888
|
else {
|
|
806
|
-
merged.push(...chunks.filter((c) => c !==
|
|
889
|
+
merged.push(...chunks.filter((c) => c !== ""));
|
|
807
890
|
}
|
|
808
891
|
renderer.setToolCall(event.callId, { ...existing, liveOutput: merged });
|
|
809
892
|
break;
|
|
810
893
|
}
|
|
811
|
-
case
|
|
894
|
+
case "tool_call_end": {
|
|
812
895
|
const toolName = callIdToToolName.get(event.callId) ?? event.callId;
|
|
813
896
|
const prevTc = renderer.getToolCall(event.callId);
|
|
814
|
-
const
|
|
897
|
+
const _elapsed = prevTc?.startedAt ? Math.floor((Date.now() - prevTc.startedAt) / 1000) : 0;
|
|
815
898
|
renderer.setToolCall(event.callId, {
|
|
816
899
|
toolName,
|
|
817
|
-
status: event.isError ?
|
|
900
|
+
status: event.isError ? "error" : "done",
|
|
818
901
|
output: event.output?.slice(0, 500),
|
|
819
902
|
args: prevTc?.args,
|
|
820
903
|
resultSummary: event.output ? summarizeToolOutput(event.output) : undefined,
|
|
821
904
|
startedAt: prevTc?.startedAt,
|
|
822
905
|
});
|
|
823
|
-
cybergotchiEvents.emit(
|
|
906
|
+
cybergotchiEvents.emit("cybergotchi", { type: event.isError ? "toolError" : "toolSuccess", toolName });
|
|
824
907
|
// Auto-commit with file list
|
|
825
908
|
if (!event.isError && isGitRepo()) {
|
|
826
|
-
const rawArgs = prevTc?.args ??
|
|
827
|
-
const filePath = rawArgs.startsWith(
|
|
909
|
+
const rawArgs = prevTc?.args ?? "";
|
|
910
|
+
const filePath = rawArgs.startsWith("$") ? null : rawArgs;
|
|
828
911
|
const hash = autoCommitAIEdits(toolName, filePath ? [filePath] : [], process.cwd());
|
|
829
912
|
if (hash) {
|
|
830
913
|
// Show changed files in commit message
|
|
831
914
|
let commitMsg = `git: committed ${hash}`;
|
|
832
915
|
try {
|
|
833
|
-
const { execSync } = await import(
|
|
834
|
-
const files = execSync(`git diff-tree --no-commit-id --name-only -r ${hash}`, {
|
|
916
|
+
const { execSync } = await import("node:child_process");
|
|
917
|
+
const files = execSync(`git diff-tree --no-commit-id --name-only -r ${hash}`, {
|
|
918
|
+
encoding: "utf-8",
|
|
919
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
920
|
+
}).trim();
|
|
835
921
|
if (files)
|
|
836
|
-
commitMsg += `\n${files
|
|
922
|
+
commitMsg += `\n${files
|
|
923
|
+
.split("\n")
|
|
924
|
+
.map((f) => ` ${f}`)
|
|
925
|
+
.join("\n")}`;
|
|
926
|
+
}
|
|
927
|
+
catch {
|
|
928
|
+
/* ignore */
|
|
837
929
|
}
|
|
838
|
-
catch { /* ignore */ }
|
|
839
930
|
messages = [...messages, createInfoMessage(commitMsg)];
|
|
840
|
-
cybergotchiEvents.emit(
|
|
931
|
+
cybergotchiEvents.emit("cybergotchi", { type: "commit" });
|
|
841
932
|
}
|
|
842
933
|
}
|
|
843
934
|
break;
|
|
844
935
|
}
|
|
845
|
-
case
|
|
936
|
+
case "cost_update":
|
|
846
937
|
currentModel = event.model;
|
|
847
|
-
cost.record(
|
|
938
|
+
cost.record("provider", event.model, event.inputTokens, event.outputTokens, event.cost || estimateCost(event.model, event.inputTokens, event.outputTokens));
|
|
848
939
|
renderer.setTokenCount(cost.totalOutputTokens);
|
|
849
940
|
syncRenderer();
|
|
850
941
|
break;
|
|
851
|
-
case
|
|
942
|
+
case "rate_limited":
|
|
852
943
|
renderer.setError(`⏳ Rate limited — retrying in ${event.retryIn}s (attempt ${event.attempt}/3)`);
|
|
853
944
|
break;
|
|
854
|
-
case
|
|
945
|
+
case "error":
|
|
855
946
|
renderer.setError(event.message);
|
|
856
947
|
break;
|
|
857
|
-
case
|
|
948
|
+
case "turn_complete": {
|
|
858
949
|
// Save thinking summary before clearing
|
|
859
950
|
const thinkElapsed = renderer.getThinkingStartedAt()
|
|
860
951
|
? Math.floor((Date.now() - renderer.getThinkingStartedAt()) / 1000)
|
|
@@ -865,7 +956,7 @@ export async function startREPL(config) {
|
|
|
865
956
|
else {
|
|
866
957
|
renderer.setLastThinkingSummary(null);
|
|
867
958
|
}
|
|
868
|
-
renderer.setThinkingText(
|
|
959
|
+
renderer.setThinkingText("");
|
|
869
960
|
renderer.setThinkingStartedAt(null);
|
|
870
961
|
// Finalize streaming message
|
|
871
962
|
if (accumulated) {
|
|
@@ -876,7 +967,7 @@ export async function startREPL(config) {
|
|
|
876
967
|
else {
|
|
877
968
|
messages = [...messages, createAssistantMessage(accumulated)];
|
|
878
969
|
}
|
|
879
|
-
accumulated =
|
|
970
|
+
accumulated = "";
|
|
880
971
|
}
|
|
881
972
|
else {
|
|
882
973
|
const last = messages[messages.length - 1];
|
|
@@ -884,7 +975,7 @@ export async function startREPL(config) {
|
|
|
884
975
|
messages = [...messages.slice(0, -1), { ...last, meta: {} }];
|
|
885
976
|
}
|
|
886
977
|
}
|
|
887
|
-
renderer.setStreamingText(
|
|
978
|
+
renderer.setStreamingText("");
|
|
888
979
|
// Collapse all tool calls from this turn (clean up visual noise)
|
|
889
980
|
renderer.collapseAllToolCalls();
|
|
890
981
|
// Save session
|
|
@@ -893,7 +984,9 @@ export async function startREPL(config) {
|
|
|
893
984
|
try {
|
|
894
985
|
saveSession(session);
|
|
895
986
|
}
|
|
896
|
-
catch {
|
|
987
|
+
catch {
|
|
988
|
+
/* ignore */
|
|
989
|
+
}
|
|
897
990
|
break;
|
|
898
991
|
}
|
|
899
992
|
}
|
|
@@ -912,14 +1005,14 @@ export async function startREPL(config) {
|
|
|
912
1005
|
messages = [...messages.slice(0, -1), { ...last, content: last.content + accumulated, meta: {} }];
|
|
913
1006
|
}
|
|
914
1007
|
else {
|
|
915
|
-
messages = [...messages, createAssistantMessage(accumulated
|
|
1008
|
+
messages = [...messages, createAssistantMessage(`${accumulated}\n\n[interrupted]`)];
|
|
916
1009
|
}
|
|
917
|
-
accumulated =
|
|
1010
|
+
accumulated = "";
|
|
918
1011
|
}
|
|
919
1012
|
loading = false;
|
|
920
1013
|
abortController = null;
|
|
921
1014
|
renderer.setLoading(false);
|
|
922
|
-
renderer.setStreamingText(
|
|
1015
|
+
renderer.setStreamingText("");
|
|
923
1016
|
// Content auto-scrolls via terminal native scrollback
|
|
924
1017
|
syncRenderer();
|
|
925
1018
|
}
|
|
@@ -938,12 +1031,21 @@ export async function startREPL(config) {
|
|
|
938
1031
|
try {
|
|
939
1032
|
saveSession(session);
|
|
940
1033
|
}
|
|
941
|
-
catch {
|
|
1034
|
+
catch {
|
|
1035
|
+
/* ignore */
|
|
1036
|
+
}
|
|
942
1037
|
}
|
|
943
1038
|
// Ensure terminal restoration on unexpected exit
|
|
944
|
-
process.on(
|
|
945
|
-
process.on(
|
|
946
|
-
|
|
1039
|
+
process.on("exit", cleanup);
|
|
1040
|
+
process.on("SIGTERM", () => {
|
|
1041
|
+
cleanup();
|
|
1042
|
+
process.exit(143);
|
|
1043
|
+
});
|
|
1044
|
+
process.on("uncaughtException", (err) => {
|
|
1045
|
+
cleanup();
|
|
1046
|
+
console.error("Fatal:", err);
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
});
|
|
947
1049
|
// Start
|
|
948
1050
|
renderer.start();
|
|
949
1051
|
// Banner is already printed to stdout by main.tsx (visible in terminal scrollback)
|