tinker-agent 1.0.65 → 1.2.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/CHANGELOG.md +36 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +90 -9
- package/bin/tinker.js +39 -0
- package/node_modules/ansi-regex/index.d.ts +33 -0
- package/node_modules/ansi-regex/index.js +14 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +61 -0
- package/node_modules/ansi-regex/readme.md +66 -0
- package/node_modules/ansi-styles/index.d.ts +236 -0
- package/node_modules/ansi-styles/index.js +223 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +54 -0
- package/node_modules/ansi-styles/readme.md +173 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/has-flag/index.d.ts +39 -0
- package/node_modules/has-flag/index.js +8 -0
- package/node_modules/has-flag/license +9 -0
- package/node_modules/has-flag/package.json +49 -0
- package/node_modules/has-flag/readme.md +74 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +12 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +53 -0
- package/node_modules/is-fullwidth-code-point/readme.md +31 -0
- package/node_modules/markdansi/LICENSE +21 -0
- package/node_modules/markdansi/README.md +167 -0
- package/node_modules/markdansi/dist/ast.d.ts +99 -0
- package/node_modules/markdansi/dist/ast.js +1 -0
- package/node_modules/markdansi/dist/cli.d.ts +17 -0
- package/node_modules/markdansi/dist/cli.js +208 -0
- package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
- package/node_modules/markdansi/dist/hyperlink.js +26 -0
- package/node_modules/markdansi/dist/index.d.ts +10 -0
- package/node_modules/markdansi/dist/index.js +16 -0
- package/node_modules/markdansi/dist/parser.d.ts +2 -0
- package/node_modules/markdansi/dist/parser.js +205 -0
- package/node_modules/markdansi/dist/render.d.ts +9 -0
- package/node_modules/markdansi/dist/render.js +680 -0
- package/node_modules/markdansi/dist/stream.d.ts +39 -0
- package/node_modules/markdansi/dist/stream.js +192 -0
- package/node_modules/markdansi/dist/theme.d.ts +18 -0
- package/node_modules/markdansi/dist/theme.js +105 -0
- package/node_modules/markdansi/dist/types.d.ts +58 -0
- package/node_modules/markdansi/dist/types.js +1 -0
- package/node_modules/markdansi/dist/wrap.d.ts +10 -0
- package/node_modules/markdansi/dist/wrap.js +73 -0
- package/node_modules/markdansi/docs/spec.md +110 -0
- package/node_modules/markdansi/package.json +78 -0
- package/node_modules/markdansi/tsconfig.json +18 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/node_modules/slice-ansi/index.d.ts +19 -0
- package/node_modules/slice-ansi/index.js +317 -0
- package/node_modules/slice-ansi/license +10 -0
- package/node_modules/slice-ansi/package.json +59 -0
- package/node_modules/slice-ansi/readme.md +55 -0
- package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
- package/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/string-width/index.js +203 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +65 -0
- package/node_modules/string-width/readme.md +66 -0
- package/node_modules/strip-ansi/index.d.ts +15 -0
- package/node_modules/strip-ansi/index.js +19 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +59 -0
- package/node_modules/strip-ansi/readme.md +37 -0
- package/node_modules/supports-color/browser.d.ts +1 -0
- package/node_modules/supports-color/browser.js +35 -0
- package/node_modules/supports-color/index.d.ts +55 -0
- package/node_modules/supports-color/index.js +202 -0
- package/node_modules/supports-color/license +9 -0
- package/node_modules/supports-color/package.json +64 -0
- package/node_modules/supports-color/readme.md +75 -0
- package/node_modules/supports-hyperlinks/browser.js +10 -0
- package/node_modules/supports-hyperlinks/index.d.ts +25 -0
- package/node_modules/supports-hyperlinks/index.js +163 -0
- package/node_modules/supports-hyperlinks/license +10 -0
- package/node_modules/supports-hyperlinks/package.json +47 -0
- package/node_modules/supports-hyperlinks/readme.md +45 -0
- package/package.json +41 -10
- package/patches/markdansi@0.3.2.patch +22 -2
- package/src/agent/context-builder.ts +24 -10
- package/src/agent/context-meter.ts +89 -5
- package/src/agent/loop.ts +107 -10
- package/src/agent/runtime-session.ts +1437 -82
- package/src/agent/session-ledger.ts +169 -29
- package/src/agent/types.ts +12 -1
- package/src/agent/user-prompt-projection.ts +62 -0
- package/src/cli/config.ts +57 -14
- package/src/cli/model-profiles.ts +124 -0
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/tui-runner.tsx +68 -30
- package/src/context/compiled-context-hash.ts +1 -1
- package/src/context/compiled-context-validator.ts +92 -15
- package/src/context/context-automation-policy.ts +121 -0
- package/src/context/context-manager.ts +390 -11
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +11 -1
- package/src/context/context-revision-compiler.ts +190 -27
- package/src/context/context-revision.ts +105 -19
- package/src/context/context-surface.ts +329 -0
- package/src/context/prefix-retirement-planner.ts +570 -0
- package/src/context/protocol-frame.ts +30 -3
- package/src/context/recall-retirement-contract.ts +27 -0
- package/src/context/swap-planner.ts +34 -10
- package/src/events/observation-text-log.ts +3 -3
- package/src/events/stdout-event-printer.ts +39 -3
- package/src/events/types.ts +172 -35
- package/src/ids/runtime-id.ts +6 -0
- package/src/ids/uuid-v7.ts +7 -0
- package/src/image/image-asset-store.ts +319 -0
- package/src/image/image-input-policy.ts +14 -0
- package/src/image/image-probe.ts +233 -0
- package/src/image/image-types.ts +262 -0
- package/src/instructions/project-instructions.ts +4 -1
- package/src/mcp/mcp-manager.ts +83 -19
- package/src/model/fake-model-client.ts +9 -1
- package/src/model/input-token-estimator.ts +25 -0
- package/src/model/model-client.ts +60 -0
- package/src/model/model-request-preflight.ts +1 -0
- package/src/model/moonshot-input-token-estimator.ts +107 -0
- package/src/model/openai-chat-mapping.ts +77 -1
- package/src/model/openai-chat-model-client.ts +389 -59
- package/src/model/openai-chat-stream.ts +272 -0
- package/src/model/prompt-prefix-hash.ts +2 -4
- package/src/model/token-estimator.ts +4 -0
- package/src/observation/observation-builder.ts +29 -0
- package/src/session/resume-projection.ts +94 -34
- package/src/session/session-catalog.ts +4 -4
- package/src/session/session-errors.ts +1 -1
- package/src/session/session-history-reader.ts +151 -4
- package/src/session/session-last-response-reader.ts +189 -0
- package/src/session/session-schema.ts +349 -35
- package/src/session/session-store.ts +3266 -255
- package/src/session/sqlite-session-ledger.ts +9 -4
- package/src/skills/skill-catalog.ts +182 -0
- package/src/skills/skill-context.ts +364 -0
- package/src/skills/skill-loader.ts +779 -0
- package/src/skills/skill-tool.ts +297 -0
- package/src/tools/edit.ts +51 -64
- package/src/tools/ensure-parent-directory.ts +6 -0
- package/src/tools/read.ts +2 -2
- package/src/tools/recall.ts +43 -40
- package/src/tools/registry.ts +24 -3
- package/src/tools/ripgrep.ts +3 -2
- package/src/tools/types.ts +43 -4
- package/src/tools/write.ts +20 -31
- package/src/tui/app.tsx +333 -96
- package/src/tui/clipboard.ts +5 -0
- package/src/tui/components/mcp-panel.tsx +28 -0
- package/src/tui/components/prompt-input.tsx +790 -257
- package/src/tui/components/skills-panel.tsx +31 -0
- package/src/tui/components/timeline.tsx +30 -1
- package/src/tui/event-store.ts +85 -2
- package/src/tui/file-mention.ts +234 -0
- package/src/tui/line-editor.ts +1 -0
- package/src/tui/project-slash-commands.ts +229 -0
- package/src/tui/prompt-draft.ts +510 -0
- package/src/tui/prompt-history.ts +294 -22
- package/src/tui/slash-commands.ts +46 -2
- package/src/tui/tui-session-controller.ts +110 -48
- package/src/tui/workspace-file-search.ts +94 -0
package/src/tui/app.tsx
CHANGED
|
@@ -4,8 +4,12 @@ import { TurnCancelledError } from "../agent/turn-cancellation";
|
|
|
4
4
|
import {
|
|
5
5
|
ContextManagerError,
|
|
6
6
|
type ContextCompactionResult,
|
|
7
|
+
type ContextRetirementResult,
|
|
7
8
|
} from "../context/context-manager";
|
|
8
9
|
import { ContextBudgetExceededError } from "../model/model-request-preflight";
|
|
10
|
+
import { ModelRequestMediaAggregateError } from "../model/model-client";
|
|
11
|
+
import type { SessionId } from "../ids/runtime-id";
|
|
12
|
+
import { readLastAssistantResponse } from "../session/session-last-response-reader";
|
|
9
13
|
import { visibleTimelineItems } from "./event-store";
|
|
10
14
|
import type { PromptHistory } from "./prompt-history";
|
|
11
15
|
import { Footer } from "./components/footer";
|
|
@@ -14,25 +18,44 @@ import { BackgroundTasks } from "./components/background-tasks";
|
|
|
14
18
|
import { Header } from "./components/header";
|
|
15
19
|
import { ModelPicker } from "./components/model-picker";
|
|
16
20
|
import { FileViewer, FileViewerLoading } from "./components/file-viewer";
|
|
17
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
PromptInput,
|
|
23
|
+
type PromptMaintenanceAction,
|
|
24
|
+
type PromptSubmission,
|
|
25
|
+
type PromptSubmissionOutcome,
|
|
26
|
+
} from "./components/prompt-input";
|
|
27
|
+
import { createPromptDraft } from "./prompt-draft";
|
|
28
|
+
import { SkillsPanel } from "./components/skills-panel";
|
|
29
|
+
import { McpPanel } from "./components/mcp-panel";
|
|
18
30
|
import {
|
|
19
31
|
ResumeSessionPicker,
|
|
20
32
|
ResumeSessionPickerLoading,
|
|
21
33
|
} from "./components/resume-session-picker";
|
|
22
34
|
import { Timeline } from "./components/timeline";
|
|
23
35
|
import { parseSlashCommand, SLASH_COMMANDS } from "./slash-commands";
|
|
36
|
+
import {
|
|
37
|
+
resolveProjectSlashCommand,
|
|
38
|
+
type ProjectSlashCommand,
|
|
39
|
+
} from "./project-slash-commands";
|
|
24
40
|
import type { TuiSessionController } from "./tui-session-controller";
|
|
25
41
|
import type { SessionSummary } from "../session/session-catalog";
|
|
26
42
|
import type { ModelProfile, ModelProfiles } from "../cli/model-profiles";
|
|
27
43
|
import { loadViewFile, type ViewFile } from "./view-file";
|
|
44
|
+
import { writeClipboardText } from "./clipboard";
|
|
28
45
|
|
|
29
46
|
export type AppProps = {
|
|
30
47
|
sessionController: TuiSessionController;
|
|
31
48
|
readGitBranch?: (workspaceRoot: string) => Promise<string | undefined>;
|
|
32
49
|
history?: PromptHistory;
|
|
50
|
+
projectSlashCommands?: readonly ProjectSlashCommand[];
|
|
33
51
|
profiles?: ModelProfiles;
|
|
34
52
|
persistDefaultProfile?: (profileName: string) => Promise<void>;
|
|
35
53
|
readViewFile?: (workspaceRoot: string, filePath: string) => Promise<ViewFile>;
|
|
54
|
+
readLastResponse?: (
|
|
55
|
+
workspaceRoot: string,
|
|
56
|
+
sessionId: SessionId,
|
|
57
|
+
) => Promise<string | undefined>;
|
|
58
|
+
writeClipboard?: (markdown: string) => Promise<void>;
|
|
36
59
|
onQuit?: () => void;
|
|
37
60
|
};
|
|
38
61
|
|
|
@@ -72,9 +95,12 @@ export function App(props: AppProps) {
|
|
|
72
95
|
);
|
|
73
96
|
const [isRunning, setIsRunning] = useState(false);
|
|
74
97
|
const [isSessionOperation, setIsSessionOperation] = useState(false);
|
|
98
|
+
const [isCopying, setIsCopying] = useState(false);
|
|
75
99
|
const [isCancelling, setIsCancelling] = useState(false);
|
|
76
100
|
const [notice, setNotice] = useState<string | undefined>(undefined);
|
|
77
101
|
const [showStatus, setShowStatus] = useState(false);
|
|
102
|
+
const [showSkills, setShowSkills] = useState(false);
|
|
103
|
+
const [showMcp, setShowMcp] = useState(false);
|
|
78
104
|
const [resumePicker, setResumePicker] = useState<ResumePickerState | undefined>(
|
|
79
105
|
undefined,
|
|
80
106
|
);
|
|
@@ -97,9 +123,10 @@ export function App(props: AppProps) {
|
|
|
97
123
|
props.profiles !== undefined &&
|
|
98
124
|
props.profiles.profiles.size > 1;
|
|
99
125
|
|
|
100
|
-
const
|
|
101
|
-
?
|
|
102
|
-
: SLASH_COMMANDS.filter((
|
|
126
|
+
const builtInCommands = canSwitchModel
|
|
127
|
+
? SLASH_COMMANDS
|
|
128
|
+
: SLASH_COMMANDS.filter((command) => command.name !== "model");
|
|
129
|
+
const availableCommands = [...builtInCommands, ...(props.projectSlashCommands ?? [])];
|
|
103
130
|
|
|
104
131
|
const profileList = props.profiles ? [...props.profiles.profiles.values()] : [];
|
|
105
132
|
|
|
@@ -269,102 +296,76 @@ export function App(props: AppProps) {
|
|
|
269
296
|
});
|
|
270
297
|
};
|
|
271
298
|
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
setNotice(undefined);
|
|
287
|
-
|
|
288
|
-
if (command.type === "view") {
|
|
289
|
-
openFileView(command.filePath);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
if (command.type === "status") {
|
|
293
|
-
setShowStatus(true);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
if (command.type === "compact") {
|
|
297
|
-
setIsSessionOperation(true);
|
|
298
|
-
void props.sessionController
|
|
299
|
-
.compact()
|
|
300
|
-
.then((result) => setNotice(formatContextCompactionNotice(result)))
|
|
301
|
-
.catch((error: unknown) => {
|
|
302
|
-
setNotice(formatContextCompactionFailureNotice(error));
|
|
303
|
-
})
|
|
304
|
-
.finally(() => setIsSessionOperation(false));
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (command.type === "quit") {
|
|
308
|
-
props.onQuit?.();
|
|
309
|
-
exit();
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
if (command.type === "model" || command.type === "model_switch") {
|
|
313
|
-
if (!canSwitchModel) {
|
|
314
|
-
setNotice(
|
|
315
|
-
"Cannot switch models after the session has turns or while running.",
|
|
316
|
-
);
|
|
299
|
+
const copyLastResponse = () => {
|
|
300
|
+
setIsCopying(true);
|
|
301
|
+
void Promise.resolve()
|
|
302
|
+
.then(() =>
|
|
303
|
+
props.readLastResponse === undefined
|
|
304
|
+
? readLastAssistantResponse({
|
|
305
|
+
workspaceRoot,
|
|
306
|
+
sessionId: binding.sessionId,
|
|
307
|
+
})
|
|
308
|
+
: props.readLastResponse(workspaceRoot, binding.sessionId),
|
|
309
|
+
)
|
|
310
|
+
.then(async (markdown) => {
|
|
311
|
+
if (markdown === undefined) {
|
|
312
|
+
setNotice("No assistant response is available to copy.");
|
|
317
313
|
return;
|
|
318
314
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
setModelPickerState({ isSwitching: false });
|
|
328
|
-
setShowModelPicker(true);
|
|
329
|
-
}
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
if (command.type === "resume_list") {
|
|
333
|
-
openResumePicker();
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
setIsSessionOperation(true);
|
|
337
|
-
const operation =
|
|
338
|
-
command.type === "resume"
|
|
339
|
-
? props.sessionController
|
|
340
|
-
.resume(command.sessionId)
|
|
341
|
-
.then(() => setNotice(`Resumed session ${command.sessionId}.`))
|
|
342
|
-
: props.sessionController
|
|
343
|
-
.delete(command.sessionId)
|
|
344
|
-
.then(() => setNotice(`Deleted session ${command.sessionId}.`));
|
|
345
|
-
void operation
|
|
346
|
-
.catch((error: unknown) => {
|
|
347
|
-
setNotice(`Session operation failed: ${errorMessage(error)}`);
|
|
348
|
-
})
|
|
349
|
-
.finally(() => setIsSessionOperation(false));
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (trimmed === "" || isRunning) {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
315
|
+
await (props.writeClipboard ?? writeClipboardText)(markdown);
|
|
316
|
+
setNotice("Copied last response as Markdown.");
|
|
317
|
+
})
|
|
318
|
+
.catch((error: unknown) => {
|
|
319
|
+
setNotice(`Copy failed: ${errorMessage(error)}`);
|
|
320
|
+
})
|
|
321
|
+
.finally(() => setIsCopying(false));
|
|
322
|
+
};
|
|
356
323
|
|
|
324
|
+
const submitAgentPrompt = async (
|
|
325
|
+
submission: PromptSubmission,
|
|
326
|
+
admissionSignal: AbortSignal,
|
|
327
|
+
): Promise<PromptSubmissionOutcome> => {
|
|
328
|
+
if (isRunning) return false;
|
|
357
329
|
setNotice(undefined);
|
|
358
|
-
setIsRunning(true);
|
|
359
330
|
setIsCancelling(false);
|
|
360
331
|
const controller = new AbortController();
|
|
332
|
+
const forwardAbort = () => controller.abort(admissionSignal.reason);
|
|
333
|
+
if (admissionSignal.aborted) {
|
|
334
|
+
forwardAbort();
|
|
335
|
+
} else {
|
|
336
|
+
admissionSignal.addEventListener("abort", forwardAbort, { once: true });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let accepted;
|
|
340
|
+
try {
|
|
341
|
+
if (binding.admitTurn === undefined) {
|
|
342
|
+
throw new Error("TUI session binding does not implement turn admission.");
|
|
343
|
+
}
|
|
344
|
+
accepted = await binding.admitTurn(submission.userMessage, controller.signal);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
admissionSignal.removeEventListener("abort", forwardAbort);
|
|
347
|
+
if (
|
|
348
|
+
error instanceof ContextBudgetExceededError ||
|
|
349
|
+
error instanceof ModelRequestMediaAggregateError
|
|
350
|
+
) {
|
|
351
|
+
return {
|
|
352
|
+
kind: "maintenance_offer",
|
|
353
|
+
reason:
|
|
354
|
+
error instanceof ContextBudgetExceededError ? "budget" : "media_aggregate",
|
|
355
|
+
message: error.message,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
setNotice(`Turn was not accepted: ${errorMessage(error)}`);
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
|
|
361
362
|
activeController.current = controller;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
setIsRunning(true);
|
|
364
|
+
void props.history?.append(submission.draft).catch((error: unknown) => {
|
|
365
|
+
setNotice(`Prompt history write failed: ${errorMessage(error)}`);
|
|
366
|
+
});
|
|
367
|
+
void accepted.completion
|
|
366
368
|
.catch((error: unknown) => {
|
|
367
|
-
retainNotice = true;
|
|
368
369
|
setNotice(
|
|
369
370
|
error instanceof ContextBudgetExceededError
|
|
370
371
|
? error.message
|
|
@@ -376,12 +377,202 @@ export function App(props: AppProps) {
|
|
|
376
377
|
activeController.current = undefined;
|
|
377
378
|
setIsRunning(false);
|
|
378
379
|
setIsCancelling(false);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
380
|
+
setNotice((current) =>
|
|
381
|
+
current === "Cancelling current turn..." ? undefined : current,
|
|
382
|
+
);
|
|
382
383
|
setGitBranchRefresh((current) => current + 1);
|
|
383
384
|
}
|
|
385
|
+
admissionSignal.removeEventListener("abort", forwardAbort);
|
|
384
386
|
});
|
|
387
|
+
return true;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const onMaintenance = async (
|
|
391
|
+
action: PromptMaintenanceAction,
|
|
392
|
+
signal: AbortSignal,
|
|
393
|
+
): Promise<void> => {
|
|
394
|
+
signal.throwIfAborted();
|
|
395
|
+
setIsSessionOperation(true);
|
|
396
|
+
try {
|
|
397
|
+
if (action === "compact") {
|
|
398
|
+
const result = await props.sessionController.compact();
|
|
399
|
+
signal.throwIfAborted();
|
|
400
|
+
setNotice(formatContextCompactionNotice(result));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (action === "retire") {
|
|
404
|
+
const result = await props.sessionController.retire();
|
|
405
|
+
signal.throwIfAborted();
|
|
406
|
+
setNotice(formatContextRetirementNotice(result));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
await props.sessionController.clear();
|
|
410
|
+
signal.throwIfAborted();
|
|
411
|
+
const sessionId = props.sessionController.getBinding().sessionId;
|
|
412
|
+
setGitBranchRefresh((current) => current + 1);
|
|
413
|
+
setNotice(
|
|
414
|
+
`Started new session ${sessionId}. The image Prompt was preserved and was not submitted.`,
|
|
415
|
+
);
|
|
416
|
+
} finally {
|
|
417
|
+
setIsSessionOperation(false);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const onSubmit = (
|
|
422
|
+
submission: PromptSubmission,
|
|
423
|
+
signal: AbortSignal,
|
|
424
|
+
): PromptSubmissionOutcome | Promise<PromptSubmissionOutcome> => {
|
|
425
|
+
const { userMessage } = submission;
|
|
426
|
+
const trimmed = userMessage.content.trim();
|
|
427
|
+
setShowStatus(false);
|
|
428
|
+
setShowSkills(false);
|
|
429
|
+
setShowMcp(false);
|
|
430
|
+
setViewError(undefined);
|
|
431
|
+
|
|
432
|
+
if (userMessage.attachments === undefined && trimmed.startsWith("/")) {
|
|
433
|
+
try {
|
|
434
|
+
const projectCommand = resolveProjectSlashCommand(
|
|
435
|
+
trimmed,
|
|
436
|
+
props.projectSlashCommands ?? [],
|
|
437
|
+
);
|
|
438
|
+
if (projectCommand !== undefined) {
|
|
439
|
+
return submitAgentPrompt(
|
|
440
|
+
{
|
|
441
|
+
draft: createPromptDraft(projectCommand.prompt),
|
|
442
|
+
userMessage: { role: "user", content: projectCommand.prompt },
|
|
443
|
+
},
|
|
444
|
+
signal,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const command = parseSlashCommand(trimmed);
|
|
449
|
+
setNotice(undefined);
|
|
450
|
+
|
|
451
|
+
if (command.type === "view") {
|
|
452
|
+
openFileView(command.filePath);
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
if (command.type === "copy") {
|
|
456
|
+
copyLastResponse();
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
if (command.type === "status") {
|
|
460
|
+
setShowStatus(true);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
if (command.type === "skills") {
|
|
464
|
+
setShowSkills(true);
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
if (command.type === "mcp") {
|
|
468
|
+
setShowMcp(true);
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
if (command.type === "compact") {
|
|
472
|
+
setIsSessionOperation(true);
|
|
473
|
+
void props.sessionController
|
|
474
|
+
.compact()
|
|
475
|
+
.then((result) => setNotice(formatContextCompactionNotice(result)))
|
|
476
|
+
.catch((error: unknown) => {
|
|
477
|
+
setNotice(formatContextCompactionFailureNotice(error));
|
|
478
|
+
})
|
|
479
|
+
.finally(() => setIsSessionOperation(false));
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
if (command.type === "compact_retire") {
|
|
483
|
+
setIsSessionOperation(true);
|
|
484
|
+
void props.sessionController
|
|
485
|
+
.retire()
|
|
486
|
+
.then((result) => setNotice(formatContextRetirementNotice(result)))
|
|
487
|
+
.catch((error: unknown) => {
|
|
488
|
+
setNotice(formatContextRetirementFailureNotice(error));
|
|
489
|
+
})
|
|
490
|
+
.finally(() => setIsSessionOperation(false));
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
if (command.type === "clear") {
|
|
494
|
+
setIsSessionOperation(true);
|
|
495
|
+
void props.sessionController
|
|
496
|
+
.clear()
|
|
497
|
+
.then(() => {
|
|
498
|
+
const sessionId = props.sessionController.getBinding().sessionId;
|
|
499
|
+
setGitBranchRefresh((current) => current + 1);
|
|
500
|
+
setNotice(
|
|
501
|
+
`Started new session ${sessionId}. Previous session remains available via /resume.`,
|
|
502
|
+
);
|
|
503
|
+
})
|
|
504
|
+
.catch((error: unknown) => {
|
|
505
|
+
setNotice(`Clear failed: ${errorMessage(error)}`);
|
|
506
|
+
})
|
|
507
|
+
.finally(() => setIsSessionOperation(false));
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
if (command.type === "fork") {
|
|
511
|
+
setIsSessionOperation(true);
|
|
512
|
+
void props.sessionController
|
|
513
|
+
.fork()
|
|
514
|
+
.then((sessionId) => {
|
|
515
|
+
setGitBranchRefresh((current) => current + 1);
|
|
516
|
+
setNotice(
|
|
517
|
+
`Cloned current session as ${sessionId}. Previous session remains available via /resume.`,
|
|
518
|
+
);
|
|
519
|
+
})
|
|
520
|
+
.catch((error: unknown) => {
|
|
521
|
+
setNotice(`Fork failed: ${errorMessage(error)}`);
|
|
522
|
+
})
|
|
523
|
+
.finally(() => setIsSessionOperation(false));
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
if (command.type === "quit") {
|
|
527
|
+
props.onQuit?.();
|
|
528
|
+
exit();
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
if (command.type === "model" || command.type === "model_switch") {
|
|
532
|
+
if (!canSwitchModel) {
|
|
533
|
+
setNotice(
|
|
534
|
+
"Cannot switch models after the session has turns or while running.",
|
|
535
|
+
);
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
if (command.type === "model_switch") {
|
|
539
|
+
const targetProfile = props.profiles?.profiles.get(command.profileName);
|
|
540
|
+
if (targetProfile === undefined) {
|
|
541
|
+
setNotice(`Unknown model profile: ${command.profileName}`);
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
void doSwitchModel(targetProfile);
|
|
545
|
+
} else {
|
|
546
|
+
setModelPickerState({ isSwitching: false });
|
|
547
|
+
setShowModelPicker(true);
|
|
548
|
+
}
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
if (command.type === "resume_list") {
|
|
552
|
+
openResumePicker();
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
setIsSessionOperation(true);
|
|
556
|
+
const operation =
|
|
557
|
+
command.type === "resume"
|
|
558
|
+
? props.sessionController
|
|
559
|
+
.resume(command.sessionId)
|
|
560
|
+
.then(() => setNotice(`Resumed session ${command.sessionId}.`))
|
|
561
|
+
: props.sessionController
|
|
562
|
+
.delete(command.sessionId)
|
|
563
|
+
.then(() => setNotice(`Deleted session ${command.sessionId}.`));
|
|
564
|
+
void operation
|
|
565
|
+
.catch((error: unknown) => {
|
|
566
|
+
setNotice(`Session operation failed: ${errorMessage(error)}`);
|
|
567
|
+
})
|
|
568
|
+
.finally(() => setIsSessionOperation(false));
|
|
569
|
+
} catch (error) {
|
|
570
|
+
setNotice(errorMessage(error));
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
return submitAgentPrompt(submission, signal);
|
|
385
576
|
};
|
|
386
577
|
|
|
387
578
|
return (
|
|
@@ -421,6 +612,16 @@ export function App(props: AppProps) {
|
|
|
421
612
|
<ContextStatus state={state} />
|
|
422
613
|
</Box>
|
|
423
614
|
) : null}
|
|
615
|
+
{showSkills ? (
|
|
616
|
+
<Box marginTop={1}>
|
|
617
|
+
<SkillsPanel snapshot={binding.skills()} />
|
|
618
|
+
</Box>
|
|
619
|
+
) : null}
|
|
620
|
+
{showMcp ? (
|
|
621
|
+
<Box marginTop={1}>
|
|
622
|
+
<McpPanel snapshot={binding.mcp()} />
|
|
623
|
+
</Box>
|
|
624
|
+
) : null}
|
|
424
625
|
<Box marginTop={1}>
|
|
425
626
|
<Footer
|
|
426
627
|
status={isCancelling ? "cancelling" : state.status}
|
|
@@ -443,10 +644,13 @@ export function App(props: AppProps) {
|
|
|
443
644
|
workspaceRoot={binding.workspaceRoot}
|
|
444
645
|
gitBranch={gitBranch}
|
|
445
646
|
contextUsage={state.contextUsage}
|
|
446
|
-
isDisabled={isRunning || isSessionOperation}
|
|
647
|
+
isDisabled={isRunning || isSessionOperation || isCopying}
|
|
447
648
|
history={props.history}
|
|
448
649
|
commands={availableCommands}
|
|
650
|
+
importImage={binding.importImage}
|
|
651
|
+
verifyImageAssets={binding.verifyImageAssets}
|
|
449
652
|
onSubmit={onSubmit}
|
|
653
|
+
onMaintenance={onMaintenance}
|
|
450
654
|
placeholder='Enter a coding request, or "/" for commands'
|
|
451
655
|
/>
|
|
452
656
|
)}
|
|
@@ -495,3 +699,36 @@ export function formatContextCompactionFailureNotice(error: unknown): string {
|
|
|
495
699
|
: "CONTEXT_COMPACTION_FAILED";
|
|
496
700
|
return `Context compaction failed at ${error.stage} (${code}).`;
|
|
497
701
|
}
|
|
702
|
+
|
|
703
|
+
export function formatContextRetirementNotice(result: ContextRetirementResult): string {
|
|
704
|
+
if (result.status === "unchanged") {
|
|
705
|
+
if (result.outcome === "below_target") {
|
|
706
|
+
return `Context is already below the retirement target (${result.guardedTokensBefore.toLocaleString("en-US")} <= ${result.targetTokens.toLocaleString("en-US")} estimated tokens).`;
|
|
707
|
+
}
|
|
708
|
+
return "No complete cold prefix can be retired; the most recent 8 completed turns are protected.";
|
|
709
|
+
}
|
|
710
|
+
const before = result.guardedTokensBefore.toLocaleString("en-US");
|
|
711
|
+
const after = result.guardedTokensAfter.toLocaleString("en-US");
|
|
712
|
+
if (result.outcome === "retirement_floor") {
|
|
713
|
+
return `Context prefix retired: revision ${result.previousRevisionNumber} -> ${result.revisionNumber}, ${result.retiredTurnCount} turns removed from the active request, ${before} -> ${after} estimated tokens; target ${result.targetTokens.toLocaleString("en-US")} was not reached. Run /compact first when retained tool output is still eligible. Older history remains available through Recall.`;
|
|
714
|
+
}
|
|
715
|
+
const reduction =
|
|
716
|
+
result.guardedTokensBefore === 0
|
|
717
|
+
? "0.0"
|
|
718
|
+
: (
|
|
719
|
+
((result.guardedTokensBefore - result.guardedTokensAfter) /
|
|
720
|
+
result.guardedTokensBefore) *
|
|
721
|
+
100
|
|
722
|
+
).toFixed(1);
|
|
723
|
+
return `Context prefix retired: revision ${result.previousRevisionNumber} -> ${result.revisionNumber}, ${result.retiredTurnCount} turns removed from the active request, ${before} -> ${after} estimated tokens (-${reduction}%). Older history remains available through Recall.`;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export function formatContextRetirementFailureNotice(error: unknown): string {
|
|
727
|
+
if (!(error instanceof ContextManagerError)) {
|
|
728
|
+
return "Context prefix retirement failed.";
|
|
729
|
+
}
|
|
730
|
+
const code = /^[A-Za-z0-9_]+$/.test(error.code)
|
|
731
|
+
? error.code
|
|
732
|
+
: "CONTEXT_RETIREMENT_FAILED";
|
|
733
|
+
return `Context prefix retirement failed at ${error.stage} (${code}).`;
|
|
734
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import type { McpInventorySnapshot } from "../../mcp/mcp-manager";
|
|
3
|
+
|
|
4
|
+
export function McpPanel(props: { snapshot: McpInventorySnapshot }) {
|
|
5
|
+
return (
|
|
6
|
+
<Box flexDirection="column">
|
|
7
|
+
<Text bold>MCP Tools</Text>
|
|
8
|
+
{props.snapshot.servers.length === 0 ? (
|
|
9
|
+
<Text color="yellow"> no MCP servers configured</Text>
|
|
10
|
+
) : (
|
|
11
|
+
props.snapshot.servers.map((server) => (
|
|
12
|
+
<Box key={server.name} flexDirection="column">
|
|
13
|
+
<Text>
|
|
14
|
+
{" "}
|
|
15
|
+
<Text bold>{server.name}</Text>
|
|
16
|
+
{` (connected, ${server.tools.length} tool${server.tools.length === 1 ? "" : "s"})`}
|
|
17
|
+
</Text>
|
|
18
|
+
<Text dimColor>
|
|
19
|
+
{server.tools.length === 0
|
|
20
|
+
? " no tools available"
|
|
21
|
+
: ` ${server.tools.join(", ")}`}
|
|
22
|
+
</Text>
|
|
23
|
+
</Box>
|
|
24
|
+
))
|
|
25
|
+
)}
|
|
26
|
+
</Box>
|
|
27
|
+
);
|
|
28
|
+
}
|