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.
Files changed (208) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +90 -9
  5. package/bin/tinker.js +39 -0
  6. package/node_modules/ansi-regex/index.d.ts +33 -0
  7. package/node_modules/ansi-regex/index.js +14 -0
  8. package/node_modules/ansi-regex/license +9 -0
  9. package/node_modules/ansi-regex/package.json +61 -0
  10. package/node_modules/ansi-regex/readme.md +66 -0
  11. package/node_modules/ansi-styles/index.d.ts +236 -0
  12. package/node_modules/ansi-styles/index.js +223 -0
  13. package/node_modules/ansi-styles/license +9 -0
  14. package/node_modules/ansi-styles/package.json +54 -0
  15. package/node_modules/ansi-styles/readme.md +173 -0
  16. package/node_modules/chalk/license +9 -0
  17. package/node_modules/chalk/package.json +83 -0
  18. package/node_modules/chalk/readme.md +297 -0
  19. package/node_modules/chalk/source/index.d.ts +325 -0
  20. package/node_modules/chalk/source/index.js +225 -0
  21. package/node_modules/chalk/source/utilities.js +33 -0
  22. package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  23. package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  24. package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  25. package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  26. package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  27. package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  28. package/node_modules/character-entities/index.d.ts +6 -0
  29. package/node_modules/character-entities/index.js +2132 -0
  30. package/node_modules/character-entities/license +22 -0
  31. package/node_modules/character-entities/package.json +78 -0
  32. package/node_modules/character-entities/readme.md +152 -0
  33. package/node_modules/decode-named-character-reference/index.d.ts +13 -0
  34. package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
  35. package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
  36. package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
  37. package/node_modules/decode-named-character-reference/index.dom.js +32 -0
  38. package/node_modules/decode-named-character-reference/index.js +19 -0
  39. package/node_modules/decode-named-character-reference/license +22 -0
  40. package/node_modules/decode-named-character-reference/package.json +90 -0
  41. package/node_modules/decode-named-character-reference/readme.md +136 -0
  42. package/node_modules/get-east-asian-width/index.d.ts +60 -0
  43. package/node_modules/get-east-asian-width/index.js +30 -0
  44. package/node_modules/get-east-asian-width/license +9 -0
  45. package/node_modules/get-east-asian-width/lookup-data.js +21 -0
  46. package/node_modules/get-east-asian-width/lookup.js +138 -0
  47. package/node_modules/get-east-asian-width/package.json +71 -0
  48. package/node_modules/get-east-asian-width/readme.md +65 -0
  49. package/node_modules/get-east-asian-width/utilities.js +24 -0
  50. package/node_modules/has-flag/index.d.ts +39 -0
  51. package/node_modules/has-flag/index.js +8 -0
  52. package/node_modules/has-flag/license +9 -0
  53. package/node_modules/has-flag/package.json +49 -0
  54. package/node_modules/has-flag/readme.md +74 -0
  55. package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
  56. package/node_modules/is-fullwidth-code-point/index.js +12 -0
  57. package/node_modules/is-fullwidth-code-point/license +9 -0
  58. package/node_modules/is-fullwidth-code-point/package.json +53 -0
  59. package/node_modules/is-fullwidth-code-point/readme.md +31 -0
  60. package/node_modules/markdansi/LICENSE +21 -0
  61. package/node_modules/markdansi/README.md +167 -0
  62. package/node_modules/markdansi/dist/ast.d.ts +99 -0
  63. package/node_modules/markdansi/dist/ast.js +1 -0
  64. package/node_modules/markdansi/dist/cli.d.ts +17 -0
  65. package/node_modules/markdansi/dist/cli.js +208 -0
  66. package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
  67. package/node_modules/markdansi/dist/hyperlink.js +26 -0
  68. package/node_modules/markdansi/dist/index.d.ts +10 -0
  69. package/node_modules/markdansi/dist/index.js +16 -0
  70. package/node_modules/markdansi/dist/parser.d.ts +2 -0
  71. package/node_modules/markdansi/dist/parser.js +205 -0
  72. package/node_modules/markdansi/dist/render.d.ts +9 -0
  73. package/node_modules/markdansi/dist/render.js +680 -0
  74. package/node_modules/markdansi/dist/stream.d.ts +39 -0
  75. package/node_modules/markdansi/dist/stream.js +192 -0
  76. package/node_modules/markdansi/dist/theme.d.ts +18 -0
  77. package/node_modules/markdansi/dist/theme.js +105 -0
  78. package/node_modules/markdansi/dist/types.d.ts +58 -0
  79. package/node_modules/markdansi/dist/types.js +1 -0
  80. package/node_modules/markdansi/dist/wrap.d.ts +10 -0
  81. package/node_modules/markdansi/dist/wrap.js +73 -0
  82. package/node_modules/markdansi/docs/spec.md +110 -0
  83. package/node_modules/markdansi/package.json +78 -0
  84. package/node_modules/markdansi/tsconfig.json +18 -0
  85. package/node_modules/marked/LICENSE +44 -0
  86. package/node_modules/marked/README.md +115 -0
  87. package/node_modules/marked/bin/main.js +284 -0
  88. package/node_modules/marked/bin/marked.js +16 -0
  89. package/node_modules/marked/lib/marked.d.ts +760 -0
  90. package/node_modules/marked/lib/marked.esm.js +77 -0
  91. package/node_modules/marked/lib/marked.esm.js.map +7 -0
  92. package/node_modules/marked/lib/marked.umd.js +79 -0
  93. package/node_modules/marked/lib/marked.umd.js.map +7 -0
  94. package/node_modules/marked/man/marked.1 +113 -0
  95. package/node_modules/marked/man/marked.1.md +93 -0
  96. package/node_modules/marked/package.json +103 -0
  97. package/node_modules/slice-ansi/index.d.ts +19 -0
  98. package/node_modules/slice-ansi/index.js +317 -0
  99. package/node_modules/slice-ansi/license +10 -0
  100. package/node_modules/slice-ansi/package.json +59 -0
  101. package/node_modules/slice-ansi/readme.md +55 -0
  102. package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
  103. package/node_modules/string-width/index.d.ts +39 -0
  104. package/node_modules/string-width/index.js +203 -0
  105. package/node_modules/string-width/license +9 -0
  106. package/node_modules/string-width/package.json +65 -0
  107. package/node_modules/string-width/readme.md +66 -0
  108. package/node_modules/strip-ansi/index.d.ts +15 -0
  109. package/node_modules/strip-ansi/index.js +19 -0
  110. package/node_modules/strip-ansi/license +9 -0
  111. package/node_modules/strip-ansi/package.json +59 -0
  112. package/node_modules/strip-ansi/readme.md +37 -0
  113. package/node_modules/supports-color/browser.d.ts +1 -0
  114. package/node_modules/supports-color/browser.js +35 -0
  115. package/node_modules/supports-color/index.d.ts +55 -0
  116. package/node_modules/supports-color/index.js +202 -0
  117. package/node_modules/supports-color/license +9 -0
  118. package/node_modules/supports-color/package.json +64 -0
  119. package/node_modules/supports-color/readme.md +75 -0
  120. package/node_modules/supports-hyperlinks/browser.js +10 -0
  121. package/node_modules/supports-hyperlinks/index.d.ts +25 -0
  122. package/node_modules/supports-hyperlinks/index.js +163 -0
  123. package/node_modules/supports-hyperlinks/license +10 -0
  124. package/node_modules/supports-hyperlinks/package.json +47 -0
  125. package/node_modules/supports-hyperlinks/readme.md +45 -0
  126. package/package.json +41 -10
  127. package/patches/markdansi@0.3.2.patch +22 -2
  128. package/src/agent/context-builder.ts +24 -10
  129. package/src/agent/context-meter.ts +89 -5
  130. package/src/agent/loop.ts +107 -10
  131. package/src/agent/runtime-session.ts +1437 -82
  132. package/src/agent/session-ledger.ts +169 -29
  133. package/src/agent/types.ts +12 -1
  134. package/src/agent/user-prompt-projection.ts +62 -0
  135. package/src/cli/config.ts +57 -14
  136. package/src/cli/model-profiles.ts +124 -0
  137. package/src/cli/run-runner.ts +4 -1
  138. package/src/cli/tui-runner.tsx +68 -30
  139. package/src/context/compiled-context-hash.ts +1 -1
  140. package/src/context/compiled-context-validator.ts +92 -15
  141. package/src/context/context-automation-policy.ts +121 -0
  142. package/src/context/context-manager.ts +390 -11
  143. package/src/context/context-policy.ts +8 -0
  144. package/src/context/context-protocol-validator.ts +11 -1
  145. package/src/context/context-revision-compiler.ts +190 -27
  146. package/src/context/context-revision.ts +105 -19
  147. package/src/context/context-surface.ts +329 -0
  148. package/src/context/prefix-retirement-planner.ts +570 -0
  149. package/src/context/protocol-frame.ts +30 -3
  150. package/src/context/recall-retirement-contract.ts +27 -0
  151. package/src/context/swap-planner.ts +34 -10
  152. package/src/events/observation-text-log.ts +3 -3
  153. package/src/events/stdout-event-printer.ts +39 -3
  154. package/src/events/types.ts +172 -35
  155. package/src/ids/runtime-id.ts +6 -0
  156. package/src/ids/uuid-v7.ts +7 -0
  157. package/src/image/image-asset-store.ts +319 -0
  158. package/src/image/image-input-policy.ts +14 -0
  159. package/src/image/image-probe.ts +233 -0
  160. package/src/image/image-types.ts +262 -0
  161. package/src/instructions/project-instructions.ts +4 -1
  162. package/src/mcp/mcp-manager.ts +83 -19
  163. package/src/model/fake-model-client.ts +9 -1
  164. package/src/model/input-token-estimator.ts +25 -0
  165. package/src/model/model-client.ts +60 -0
  166. package/src/model/model-request-preflight.ts +1 -0
  167. package/src/model/moonshot-input-token-estimator.ts +107 -0
  168. package/src/model/openai-chat-mapping.ts +77 -1
  169. package/src/model/openai-chat-model-client.ts +389 -59
  170. package/src/model/openai-chat-stream.ts +272 -0
  171. package/src/model/prompt-prefix-hash.ts +2 -4
  172. package/src/model/token-estimator.ts +4 -0
  173. package/src/observation/observation-builder.ts +29 -0
  174. package/src/session/resume-projection.ts +94 -34
  175. package/src/session/session-catalog.ts +4 -4
  176. package/src/session/session-errors.ts +1 -1
  177. package/src/session/session-history-reader.ts +151 -4
  178. package/src/session/session-last-response-reader.ts +189 -0
  179. package/src/session/session-schema.ts +349 -35
  180. package/src/session/session-store.ts +3266 -255
  181. package/src/session/sqlite-session-ledger.ts +9 -4
  182. package/src/skills/skill-catalog.ts +182 -0
  183. package/src/skills/skill-context.ts +364 -0
  184. package/src/skills/skill-loader.ts +779 -0
  185. package/src/skills/skill-tool.ts +297 -0
  186. package/src/tools/edit.ts +51 -64
  187. package/src/tools/ensure-parent-directory.ts +6 -0
  188. package/src/tools/read.ts +2 -2
  189. package/src/tools/recall.ts +43 -40
  190. package/src/tools/registry.ts +24 -3
  191. package/src/tools/ripgrep.ts +3 -2
  192. package/src/tools/types.ts +43 -4
  193. package/src/tools/write.ts +20 -31
  194. package/src/tui/app.tsx +333 -96
  195. package/src/tui/clipboard.ts +5 -0
  196. package/src/tui/components/mcp-panel.tsx +28 -0
  197. package/src/tui/components/prompt-input.tsx +790 -257
  198. package/src/tui/components/skills-panel.tsx +31 -0
  199. package/src/tui/components/timeline.tsx +30 -1
  200. package/src/tui/event-store.ts +85 -2
  201. package/src/tui/file-mention.ts +234 -0
  202. package/src/tui/line-editor.ts +1 -0
  203. package/src/tui/project-slash-commands.ts +229 -0
  204. package/src/tui/prompt-draft.ts +510 -0
  205. package/src/tui/prompt-history.ts +294 -22
  206. package/src/tui/slash-commands.ts +46 -2
  207. package/src/tui/tui-session-controller.ts +110 -48
  208. 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 { PromptInput } from "./components/prompt-input";
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 availableCommands = canSwitchModel
101
- ? undefined
102
- : SLASH_COMMANDS.filter((cmd) => cmd.name !== "model");
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 onSubmit = (prompt: string) => {
273
- const trimmed = prompt.trim();
274
- setShowStatus(false);
275
- setViewError(undefined);
276
-
277
- if (trimmed.startsWith("/")) {
278
- let command;
279
- try {
280
- command = parseSlashCommand(trimmed);
281
- } catch (error) {
282
- setNotice(errorMessage(error));
283
- return;
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
- if (command.type === "model_switch") {
320
- const targetProfile = props.profiles?.profiles.get(command.profileName);
321
- if (targetProfile === undefined) {
322
- setNotice(`Unknown model profile: ${command.profileName}`);
323
- return;
324
- }
325
- void doSwitchModel(targetProfile);
326
- } else {
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
- void props.history?.append(trimmed).catch(() => undefined);
363
- let retainNotice = false;
364
- void Promise.resolve()
365
- .then(() => binding.executeTurn(trimmed, controller.signal))
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
- if (!retainNotice) {
380
- setNotice(undefined);
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,5 @@
1
+ import clipboard from "clipboardy";
2
+
3
+ export async function writeClipboardText(text: string): Promise<void> {
4
+ await clipboard.write(text);
5
+ }
@@ -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
+ }