@zhijiewang/openharness 2.1.0 → 2.3.1
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 +114 -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 +42 -24
- package/dist/query/tools.js +15 -12
- package/dist/query/types.d.ts +3 -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 +311 -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 +163 -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 +25 -39
- 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.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 +5 -5
- 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.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,76 @@ 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 with LLM consolidation
|
|
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
|
+
if (currentProfile) {
|
|
685
|
+
// LLM-assisted merge: consolidate instead of blind append
|
|
686
|
+
const { createUserMessage: makeMsg } = await import("./types/message.js");
|
|
687
|
+
try {
|
|
688
|
+
const consolidated = await config.provider.complete([makeMsg(`Merge this user profile with new observations into a single cohesive profile. Keep the most important and recent information. Remove duplicates. Stay under 2000 characters. Return ONLY the merged profile text.\n\nCurrent profile:\n${currentProfile}\n\nNew observations:\n${newObservations}`)], "You are a profile curator. Return ONLY the merged profile, no commentary.", undefined, currentModel);
|
|
689
|
+
updateUserProfile(consolidated.content);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// Fallback to simple append if LLM fails
|
|
693
|
+
updateUserProfile(`${currentProfile}\n\n${newObservations}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
updateUserProfile(newObservations);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
/* learning is optional — don't block exit */
|
|
704
|
+
}
|
|
635
705
|
// Emit sessionEnd hook
|
|
636
706
|
try {
|
|
637
|
-
const { emitHookAsync } = await import(
|
|
638
|
-
await emitHookAsync(
|
|
707
|
+
const { emitHookAsync } = await import("./harness/hooks.js");
|
|
708
|
+
await emitHookAsync("sessionEnd", {
|
|
709
|
+
sessionId: session.id,
|
|
710
|
+
model: currentModel,
|
|
711
|
+
provider: config.provider.name,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
/* ignore */
|
|
639
716
|
}
|
|
640
|
-
catch { /* ignore */ }
|
|
641
717
|
cleanup();
|
|
642
718
|
process.exit(0);
|
|
643
719
|
}
|
|
@@ -653,14 +729,14 @@ export async function startREPL(config) {
|
|
|
653
729
|
messages = result.messages;
|
|
654
730
|
// Check for special commands
|
|
655
731
|
const lastMsg = messages[messages.length - 1];
|
|
656
|
-
if (lastMsg?.content ===
|
|
732
|
+
if (lastMsg?.content === "__OPEN_SESSION_BROWSER__") {
|
|
657
733
|
messages = messages.slice(0, -1);
|
|
658
734
|
renderer.openSessionBrowser();
|
|
659
735
|
syncRenderer();
|
|
660
736
|
return;
|
|
661
737
|
}
|
|
662
|
-
if (lastMsg?.content?.startsWith(
|
|
663
|
-
const themeName = lastMsg.content.split(
|
|
738
|
+
if (lastMsg?.content?.startsWith("__SWITCH_THEME__:")) {
|
|
739
|
+
const themeName = lastMsg.content.split(":")[1];
|
|
664
740
|
messages = messages.slice(0, -1);
|
|
665
741
|
setActiveTheme(themeName);
|
|
666
742
|
resetStyleCache();
|
|
@@ -668,35 +744,41 @@ export async function startREPL(config) {
|
|
|
668
744
|
resetDiffStyleCache();
|
|
669
745
|
// Persist theme to config
|
|
670
746
|
try {
|
|
671
|
-
const cfg = cachedConfig ?? {
|
|
747
|
+
const cfg = cachedConfig ?? {
|
|
748
|
+
provider: config.provider.name,
|
|
749
|
+
model: currentModel,
|
|
750
|
+
permissionMode: config.permissionMode,
|
|
751
|
+
};
|
|
672
752
|
cfg.theme = themeName;
|
|
673
753
|
writeOhConfig(cfg);
|
|
674
754
|
cachedConfig = cfg;
|
|
675
755
|
}
|
|
676
|
-
catch {
|
|
756
|
+
catch {
|
|
757
|
+
/* ignore */
|
|
758
|
+
}
|
|
677
759
|
messages = [...messages, createInfoMessage(`Theme switched to ${themeName}`)];
|
|
678
760
|
syncRenderer();
|
|
679
761
|
return;
|
|
680
762
|
}
|
|
681
|
-
if (lastMsg?.content ===
|
|
682
|
-
companionVisible = lastMsg.content ===
|
|
763
|
+
if (lastMsg?.content === "__COMPANION_OFF__" || lastMsg?.content === "__COMPANION_ON__") {
|
|
764
|
+
companionVisible = lastMsg.content === "__COMPANION_ON__";
|
|
683
765
|
messages = messages.slice(0, -1);
|
|
684
766
|
if (!companionVisible)
|
|
685
|
-
renderer.setCompanion(null,
|
|
686
|
-
messages = [...messages, createInfoMessage(`Companion ${companionVisible ?
|
|
767
|
+
renderer.setCompanion(null, "cyan");
|
|
768
|
+
messages = [...messages, createInfoMessage(`Companion ${companionVisible ? "shown" : "hidden"}`)];
|
|
687
769
|
syncRenderer();
|
|
688
770
|
return;
|
|
689
771
|
}
|
|
690
772
|
if (result.newModel)
|
|
691
773
|
currentModel = result.newModel;
|
|
692
774
|
if (result.vimToggled) {
|
|
693
|
-
vimMode = vimMode === null ?
|
|
694
|
-
messages = [...messages, createInfoMessage(vimMode ?
|
|
775
|
+
vimMode = vimMode === null ? "normal" : null;
|
|
776
|
+
messages = [...messages, createInfoMessage(vimMode ? "Vim mode ON" : "Vim mode OFF")];
|
|
695
777
|
renderer.setVimMode(vimMode);
|
|
696
778
|
}
|
|
697
779
|
if (result.fastModeToggled) {
|
|
698
780
|
fastMode = !fastMode;
|
|
699
|
-
messages = [...messages, createInfoMessage(fastMode ?
|
|
781
|
+
messages = [...messages, createInfoMessage(fastMode ? "Fast mode ON — optimized for speed" : "Fast mode OFF")];
|
|
700
782
|
}
|
|
701
783
|
// Clear old live area BEFORE syncRenderer when a query will follow.
|
|
702
784
|
// syncRenderer → scheduleRender → queueMicrotask(render). The microtask fires
|
|
@@ -721,16 +803,17 @@ export async function startREPL(config) {
|
|
|
721
803
|
renderer.setError(null);
|
|
722
804
|
renderer.clearToolCalls();
|
|
723
805
|
abortController = new AbortController();
|
|
724
|
-
let accumulated =
|
|
806
|
+
let accumulated = "";
|
|
725
807
|
const callIdToToolName = new Map();
|
|
726
808
|
const askUser = (toolName, description, riskLevel) => {
|
|
727
|
-
return renderer.askPermission(toolName, description, riskLevel ??
|
|
809
|
+
return renderer.askPermission(toolName, description, riskLevel ?? "medium");
|
|
728
810
|
};
|
|
729
811
|
const askUserQuestion = (question, options) => {
|
|
730
812
|
return renderer.askQuestion(question, options);
|
|
731
813
|
};
|
|
732
814
|
const effectiveSystemPrompt = fastMode
|
|
733
|
-
? config.systemPrompt +
|
|
815
|
+
? config.systemPrompt +
|
|
816
|
+
"\n\nIMPORTANT: Fast mode is active. Be extremely concise. Skip explanations. Go straight to the answer or action."
|
|
734
817
|
: config.systemPrompt;
|
|
735
818
|
const queryConfig = {
|
|
736
819
|
provider: config.provider,
|
|
@@ -745,116 +828,135 @@ export async function startREPL(config) {
|
|
|
745
828
|
try {
|
|
746
829
|
for await (const event of query(prompt, queryConfig, messages)) {
|
|
747
830
|
switch (event.type) {
|
|
748
|
-
case
|
|
831
|
+
case "text_delta": {
|
|
749
832
|
// Content auto-scrolls via terminal native scrollback
|
|
750
833
|
accumulated += event.content;
|
|
751
834
|
// Move completed lines to messages, keep partial in streaming
|
|
752
|
-
const lines = accumulated.split(
|
|
835
|
+
const lines = accumulated.split("\n");
|
|
753
836
|
if (lines.length > 1) {
|
|
754
|
-
const completedText = lines.slice(0, -1).join(
|
|
837
|
+
const completedText = lines.slice(0, -1).join("\n");
|
|
755
838
|
const last = messages[messages.length - 1];
|
|
756
839
|
if (last?.meta?.isStreaming) {
|
|
757
|
-
messages = [...messages.slice(0, -1), { ...last, content: last.content + completedText
|
|
840
|
+
messages = [...messages.slice(0, -1), { ...last, content: `${last.content + completedText}\n` }];
|
|
758
841
|
}
|
|
759
842
|
else {
|
|
760
|
-
messages = [
|
|
843
|
+
messages = [
|
|
844
|
+
...messages,
|
|
845
|
+
createMessage("assistant", `${completedText}\n`, { meta: { isStreaming: true } }),
|
|
846
|
+
];
|
|
761
847
|
}
|
|
762
848
|
accumulated = lines[lines.length - 1];
|
|
763
849
|
}
|
|
764
850
|
renderer.setMessages(messages);
|
|
765
851
|
renderer.setStreamingText(accumulated);
|
|
766
852
|
break;
|
|
767
|
-
|
|
853
|
+
}
|
|
854
|
+
case "thinking_delta":
|
|
768
855
|
if (!renderer.getThinkingStartedAt())
|
|
769
856
|
renderer.setThinkingStartedAt(Date.now());
|
|
770
857
|
renderer.setThinkingText(event.content);
|
|
771
858
|
break;
|
|
772
|
-
case
|
|
859
|
+
case "tool_call_start": {
|
|
773
860
|
callIdToToolName.set(event.callId, event.toolName);
|
|
774
|
-
const isAgentTool = event.toolName ===
|
|
775
|
-
renderer.setToolCall(event.callId, {
|
|
861
|
+
const isAgentTool = event.toolName === "Agent" || event.toolName === "ParallelAgents";
|
|
862
|
+
renderer.setToolCall(event.callId, {
|
|
863
|
+
toolName: event.toolName,
|
|
864
|
+
status: "running",
|
|
865
|
+
startedAt: Date.now(),
|
|
866
|
+
isAgent: isAgentTool,
|
|
867
|
+
});
|
|
776
868
|
break;
|
|
777
869
|
}
|
|
778
|
-
case
|
|
779
|
-
const tcToolName = callIdToToolName.get(event.callId) ??
|
|
870
|
+
case "tool_call_complete": {
|
|
871
|
+
const tcToolName = callIdToToolName.get(event.callId) ?? "";
|
|
780
872
|
const existingTc = renderer.getToolCall(event.callId);
|
|
781
|
-
const isAgentCall = tcToolName ===
|
|
782
|
-
const agentDesc = isAgentCall
|
|
873
|
+
const isAgentCall = tcToolName === "Agent" || tcToolName === "ParallelAgents";
|
|
874
|
+
const agentDesc = isAgentCall
|
|
875
|
+
? event.arguments.description
|
|
876
|
+
: undefined;
|
|
783
877
|
renderer.setToolCall(event.callId, {
|
|
784
878
|
...existingTc,
|
|
785
879
|
toolName: tcToolName,
|
|
786
|
-
status:
|
|
880
|
+
status: "running",
|
|
787
881
|
args: formatToolArgs(tcToolName, event.arguments),
|
|
788
882
|
agentDescription: agentDesc ?? existingTc?.agentDescription,
|
|
789
883
|
});
|
|
790
884
|
break;
|
|
791
885
|
}
|
|
792
|
-
case
|
|
886
|
+
case "tool_output_delta": {
|
|
793
887
|
// Accumulate streaming output lines
|
|
794
888
|
const existing = renderer.getToolCall(event.callId) ?? {
|
|
795
|
-
toolName: callIdToToolName.get(event.callId) ??
|
|
796
|
-
status:
|
|
889
|
+
toolName: callIdToToolName.get(event.callId) ?? "unknown",
|
|
890
|
+
status: "running",
|
|
797
891
|
};
|
|
798
892
|
const lines = existing.liveOutput ?? [];
|
|
799
|
-
const chunks = event.chunk.split(
|
|
893
|
+
const chunks = event.chunk.split("\n");
|
|
800
894
|
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 !==
|
|
895
|
+
if (merged.length > 0 && !event.chunk.startsWith("\n")) {
|
|
896
|
+
merged[merged.length - 1] = (merged[merged.length - 1] ?? "") + chunks[0];
|
|
897
|
+
merged.push(...chunks.slice(1).filter((c) => c !== ""));
|
|
804
898
|
}
|
|
805
899
|
else {
|
|
806
|
-
merged.push(...chunks.filter((c) => c !==
|
|
900
|
+
merged.push(...chunks.filter((c) => c !== ""));
|
|
807
901
|
}
|
|
808
902
|
renderer.setToolCall(event.callId, { ...existing, liveOutput: merged });
|
|
809
903
|
break;
|
|
810
904
|
}
|
|
811
|
-
case
|
|
905
|
+
case "tool_call_end": {
|
|
812
906
|
const toolName = callIdToToolName.get(event.callId) ?? event.callId;
|
|
813
907
|
const prevTc = renderer.getToolCall(event.callId);
|
|
814
|
-
const
|
|
908
|
+
const _elapsed = prevTc?.startedAt ? Math.floor((Date.now() - prevTc.startedAt) / 1000) : 0;
|
|
815
909
|
renderer.setToolCall(event.callId, {
|
|
816
910
|
toolName,
|
|
817
|
-
status: event.isError ?
|
|
911
|
+
status: event.isError ? "error" : "done",
|
|
818
912
|
output: event.output?.slice(0, 500),
|
|
819
913
|
args: prevTc?.args,
|
|
820
914
|
resultSummary: event.output ? summarizeToolOutput(event.output) : undefined,
|
|
821
915
|
startedAt: prevTc?.startedAt,
|
|
822
916
|
});
|
|
823
|
-
cybergotchiEvents.emit(
|
|
917
|
+
cybergotchiEvents.emit("cybergotchi", { type: event.isError ? "toolError" : "toolSuccess", toolName });
|
|
824
918
|
// Auto-commit with file list
|
|
825
919
|
if (!event.isError && isGitRepo()) {
|
|
826
|
-
const rawArgs = prevTc?.args ??
|
|
827
|
-
const filePath = rawArgs.startsWith(
|
|
920
|
+
const rawArgs = prevTc?.args ?? "";
|
|
921
|
+
const filePath = rawArgs.startsWith("$") ? null : rawArgs;
|
|
828
922
|
const hash = autoCommitAIEdits(toolName, filePath ? [filePath] : [], process.cwd());
|
|
829
923
|
if (hash) {
|
|
830
924
|
// Show changed files in commit message
|
|
831
925
|
let commitMsg = `git: committed ${hash}`;
|
|
832
926
|
try {
|
|
833
|
-
const { execSync } = await import(
|
|
834
|
-
const files = execSync(`git diff-tree --no-commit-id --name-only -r ${hash}`, {
|
|
927
|
+
const { execSync } = await import("node:child_process");
|
|
928
|
+
const files = execSync(`git diff-tree --no-commit-id --name-only -r ${hash}`, {
|
|
929
|
+
encoding: "utf-8",
|
|
930
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
931
|
+
}).trim();
|
|
835
932
|
if (files)
|
|
836
|
-
commitMsg += `\n${files
|
|
933
|
+
commitMsg += `\n${files
|
|
934
|
+
.split("\n")
|
|
935
|
+
.map((f) => ` ${f}`)
|
|
936
|
+
.join("\n")}`;
|
|
937
|
+
}
|
|
938
|
+
catch {
|
|
939
|
+
/* ignore */
|
|
837
940
|
}
|
|
838
|
-
catch { /* ignore */ }
|
|
839
941
|
messages = [...messages, createInfoMessage(commitMsg)];
|
|
840
|
-
cybergotchiEvents.emit(
|
|
942
|
+
cybergotchiEvents.emit("cybergotchi", { type: "commit" });
|
|
841
943
|
}
|
|
842
944
|
}
|
|
843
945
|
break;
|
|
844
946
|
}
|
|
845
|
-
case
|
|
947
|
+
case "cost_update":
|
|
846
948
|
currentModel = event.model;
|
|
847
|
-
cost.record(
|
|
949
|
+
cost.record("provider", event.model, event.inputTokens, event.outputTokens, event.cost || estimateCost(event.model, event.inputTokens, event.outputTokens));
|
|
848
950
|
renderer.setTokenCount(cost.totalOutputTokens);
|
|
849
951
|
syncRenderer();
|
|
850
952
|
break;
|
|
851
|
-
case
|
|
953
|
+
case "rate_limited":
|
|
852
954
|
renderer.setError(`⏳ Rate limited — retrying in ${event.retryIn}s (attempt ${event.attempt}/3)`);
|
|
853
955
|
break;
|
|
854
|
-
case
|
|
956
|
+
case "error":
|
|
855
957
|
renderer.setError(event.message);
|
|
856
958
|
break;
|
|
857
|
-
case
|
|
959
|
+
case "turn_complete": {
|
|
858
960
|
// Save thinking summary before clearing
|
|
859
961
|
const thinkElapsed = renderer.getThinkingStartedAt()
|
|
860
962
|
? Math.floor((Date.now() - renderer.getThinkingStartedAt()) / 1000)
|
|
@@ -865,7 +967,7 @@ export async function startREPL(config) {
|
|
|
865
967
|
else {
|
|
866
968
|
renderer.setLastThinkingSummary(null);
|
|
867
969
|
}
|
|
868
|
-
renderer.setThinkingText(
|
|
970
|
+
renderer.setThinkingText("");
|
|
869
971
|
renderer.setThinkingStartedAt(null);
|
|
870
972
|
// Finalize streaming message
|
|
871
973
|
if (accumulated) {
|
|
@@ -876,7 +978,7 @@ export async function startREPL(config) {
|
|
|
876
978
|
else {
|
|
877
979
|
messages = [...messages, createAssistantMessage(accumulated)];
|
|
878
980
|
}
|
|
879
|
-
accumulated =
|
|
981
|
+
accumulated = "";
|
|
880
982
|
}
|
|
881
983
|
else {
|
|
882
984
|
const last = messages[messages.length - 1];
|
|
@@ -884,7 +986,7 @@ export async function startREPL(config) {
|
|
|
884
986
|
messages = [...messages.slice(0, -1), { ...last, meta: {} }];
|
|
885
987
|
}
|
|
886
988
|
}
|
|
887
|
-
renderer.setStreamingText(
|
|
989
|
+
renderer.setStreamingText("");
|
|
888
990
|
// Collapse all tool calls from this turn (clean up visual noise)
|
|
889
991
|
renderer.collapseAllToolCalls();
|
|
890
992
|
// Save session
|
|
@@ -893,7 +995,9 @@ export async function startREPL(config) {
|
|
|
893
995
|
try {
|
|
894
996
|
saveSession(session);
|
|
895
997
|
}
|
|
896
|
-
catch {
|
|
998
|
+
catch {
|
|
999
|
+
/* ignore */
|
|
1000
|
+
}
|
|
897
1001
|
break;
|
|
898
1002
|
}
|
|
899
1003
|
}
|
|
@@ -912,14 +1016,14 @@ export async function startREPL(config) {
|
|
|
912
1016
|
messages = [...messages.slice(0, -1), { ...last, content: last.content + accumulated, meta: {} }];
|
|
913
1017
|
}
|
|
914
1018
|
else {
|
|
915
|
-
messages = [...messages, createAssistantMessage(accumulated
|
|
1019
|
+
messages = [...messages, createAssistantMessage(`${accumulated}\n\n[interrupted]`)];
|
|
916
1020
|
}
|
|
917
|
-
accumulated =
|
|
1021
|
+
accumulated = "";
|
|
918
1022
|
}
|
|
919
1023
|
loading = false;
|
|
920
1024
|
abortController = null;
|
|
921
1025
|
renderer.setLoading(false);
|
|
922
|
-
renderer.setStreamingText(
|
|
1026
|
+
renderer.setStreamingText("");
|
|
923
1027
|
// Content auto-scrolls via terminal native scrollback
|
|
924
1028
|
syncRenderer();
|
|
925
1029
|
}
|
|
@@ -938,12 +1042,21 @@ export async function startREPL(config) {
|
|
|
938
1042
|
try {
|
|
939
1043
|
saveSession(session);
|
|
940
1044
|
}
|
|
941
|
-
catch {
|
|
1045
|
+
catch {
|
|
1046
|
+
/* ignore */
|
|
1047
|
+
}
|
|
942
1048
|
}
|
|
943
1049
|
// Ensure terminal restoration on unexpected exit
|
|
944
|
-
process.on(
|
|
945
|
-
process.on(
|
|
946
|
-
|
|
1050
|
+
process.on("exit", cleanup);
|
|
1051
|
+
process.on("SIGTERM", () => {
|
|
1052
|
+
cleanup();
|
|
1053
|
+
process.exit(143);
|
|
1054
|
+
});
|
|
1055
|
+
process.on("uncaughtException", (err) => {
|
|
1056
|
+
cleanup();
|
|
1057
|
+
console.error("Fatal:", err);
|
|
1058
|
+
process.exit(1);
|
|
1059
|
+
});
|
|
947
1060
|
// Start
|
|
948
1061
|
renderer.start();
|
|
949
1062
|
// Banner is already printed to stdout by main.tsx (visible in terminal scrollback)
|