@sayknow-cli/coding-agent 0.3.12 → 0.3.15

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 (248) hide show
  1. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  2. package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
  3. package/dist/types/cli/classify-install-target.d.ts +0 -10
  4. package/dist/types/cli/daemon-cli.d.ts +2 -0
  5. package/dist/types/cli/initial-message.d.ts +1 -1
  6. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  7. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  8. package/dist/types/cli/notify-cli.d.ts +3 -1
  9. package/dist/types/cli/session-picker.d.ts +4 -3
  10. package/dist/types/cli/update-cli.d.ts +22 -3
  11. package/dist/types/cli.d.ts +5 -0
  12. package/dist/types/commands/daemon.d.ts +6 -2
  13. package/dist/types/commands/launch.d.ts +1 -0
  14. package/dist/types/commands/notify.d.ts +6 -0
  15. package/dist/types/commands/web-search.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
  18. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
  19. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
  20. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  21. package/dist/types/commit/analysis/summary.d.ts +1 -1
  22. package/dist/types/commit/changelog/generate.d.ts +3 -3
  23. package/dist/types/commit/changelog/index.d.ts +2 -2
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  26. package/dist/types/commit/shared-llm.d.ts +9 -9
  27. package/dist/types/config/keybindings.d.ts +134 -129
  28. package/dist/types/config/model-profile-activation.d.ts +1 -0
  29. package/dist/types/config/model-registry.d.ts +35 -35
  30. package/dist/types/config/models-config-schema.d.ts +111 -111
  31. package/dist/types/config/settings-schema.d.ts +56 -16
  32. package/dist/types/config/settings.d.ts +2 -0
  33. package/dist/types/coordinator/contract.d.ts +1 -1
  34. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  35. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  36. package/dist/types/coordinator-mcp/server.d.ts +79 -6
  37. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  38. package/dist/types/daemon/control-types.d.ts +10 -0
  39. package/dist/types/daemon/operator-contract.d.ts +47 -0
  40. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  41. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
  42. package/dist/types/discovery/helpers.d.ts +1 -1
  43. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  44. package/dist/types/edit/modes/patch.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  46. package/dist/types/goals/tools/goal-tool.d.ts +6 -6
  47. package/dist/types/i18n/messages/en.d.ts +8 -8
  48. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  49. package/dist/types/lsp/index.d.ts +9 -9
  50. package/dist/types/lsp/types.d.ts +9 -9
  51. package/dist/types/main.d.ts +66 -2
  52. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  53. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  54. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  56. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  57. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  58. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  59. package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
  60. package/dist/types/modes/components/session-selector.d.ts +22 -9
  61. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  62. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  63. package/dist/types/modes/components/status-line.d.ts +0 -10
  64. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  65. package/dist/types/modes/components/user-message.d.ts +2 -1
  66. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  67. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  68. package/dist/types/modes/interactive-mode.d.ts +24 -2
  69. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  70. package/dist/types/modes/print-mode.d.ts +3 -4
  71. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  72. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  73. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  74. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  75. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  76. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  77. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  78. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  79. package/dist/types/modes/types.d.ts +28 -2
  80. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  81. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  82. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  83. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  84. package/dist/types/notifications/config.d.ts +34 -0
  85. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  86. package/dist/types/notifications/index.d.ts +27 -0
  87. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  88. package/dist/types/notifications/notification-service.d.ts +144 -0
  89. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  90. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  91. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  92. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  93. package/dist/types/notifications/topic-registry.d.ts +24 -4
  94. package/dist/types/registry/agent-registry.d.ts +2 -0
  95. package/dist/types/rlm/index.d.ts +1 -1
  96. package/dist/types/sdk.d.ts +2 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +40 -3
  99. package/dist/types/session/cache-economics.d.ts +79 -0
  100. package/dist/types/session/context-estimation.d.ts +31 -0
  101. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  102. package/dist/types/session/session-manager.d.ts +124 -4
  103. package/dist/types/session/session-storage.d.ts +155 -13
  104. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  105. package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
  106. package/dist/types/skc-runtime/state-schema.d.ts +19 -19
  107. package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
  108. package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  109. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
  110. package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
  111. package/dist/types/task/agents.d.ts +2 -2
  112. package/dist/types/task/executor.d.ts +3 -0
  113. package/dist/types/task/receipt.d.ts +3 -1
  114. package/dist/types/task/types.d.ts +35 -27
  115. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  116. package/dist/types/tools/bash.d.ts +1 -1
  117. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  118. package/dist/types/tools/browser.d.ts +28 -28
  119. package/dist/types/tools/computer.d.ts +20 -20
  120. package/dist/types/tools/cron.d.ts +2 -2
  121. package/dist/types/tools/debug.d.ts +40 -40
  122. package/dist/types/tools/gh.d.ts +10 -10
  123. package/dist/types/tools/image-gen.d.ts +2 -2
  124. package/dist/types/tools/index.d.ts +58 -6
  125. package/dist/types/tools/monitor.d.ts +6 -6
  126. package/dist/types/tools/skill-discovery.d.ts +4 -4
  127. package/dist/types/tools/subagent.d.ts +12 -12
  128. package/dist/types/tools/telegram-send.d.ts +7 -0
  129. package/dist/types/tools/todo-write.d.ts +8 -8
  130. package/dist/types/utils/external-editor.d.ts +2 -0
  131. package/dist/types/web/insane/bridge.d.ts +9 -9
  132. package/dist/types/web/search/index.d.ts +4 -4
  133. package/package.json +8 -8
  134. package/scripts/g004-tmux-smoke.ts +196 -36
  135. package/src/cli/daemon-cli.ts +19 -29
  136. package/src/cli/malloc-env-guard.test.ts +104 -0
  137. package/src/cli/malloc-env-guard.ts +149 -0
  138. package/src/cli/notify-cli.ts +78 -14
  139. package/src/cli/session-picker.ts +23 -40
  140. package/src/cli/update-cli.ts +56 -37
  141. package/src/cli.ts +51 -10
  142. package/src/commands/daemon.ts +19 -5
  143. package/src/commands/harness.ts +515 -47
  144. package/src/commands/launch.ts +11 -3
  145. package/src/commands/notify.ts +5 -1
  146. package/src/commands/session.ts +6 -1
  147. package/src/config/keybindings.ts +5 -0
  148. package/src/config/model-profile-activation.ts +1 -1
  149. package/src/config/model-profiles.ts +14 -14
  150. package/src/config/settings-schema.ts +41 -1
  151. package/src/config/settings.ts +96 -45
  152. package/src/coordinator/contract.ts +1 -0
  153. package/src/coordinator-mcp/model-preset.ts +100 -0
  154. package/src/coordinator-mcp/policy.ts +30 -0
  155. package/src/coordinator-mcp/server.ts +2958 -430
  156. package/src/coordinator-mcp/session-reaper.ts +129 -0
  157. package/src/daemon/control-types.ts +11 -0
  158. package/src/daemon/operator-contract.ts +121 -0
  159. package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  160. package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  161. package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  162. package/src/extensibility/extensions/runner.ts +1 -1
  163. package/src/extensibility/extensions/types.ts +3 -1
  164. package/src/internal-urls/docs-index.generated.ts +10 -9
  165. package/src/internal-urls/local-protocol.ts +29 -10
  166. package/src/main.ts +301 -80
  167. package/src/modes/acp/acp-agent.ts +730 -100
  168. package/src/modes/acp/acp-mode.ts +29 -2
  169. package/src/modes/components/assistant-message.ts +32 -3
  170. package/src/modes/components/bash-execution.ts +43 -13
  171. package/src/modes/components/custom-editor.ts +5 -0
  172. package/src/modes/components/footer.ts +3 -3
  173. package/src/modes/components/hook-selector.ts +12 -0
  174. package/src/modes/components/irc-sidebar.ts +206 -0
  175. package/src/modes/components/pet-selector.ts +55 -0
  176. package/src/modes/components/sayknow-pet-widget.ts +360 -0
  177. package/src/modes/components/session-selector.ts +222 -221
  178. package/src/modes/components/settings-selector.ts +10 -0
  179. package/src/modes/components/status-line/segments.ts +14 -7
  180. package/src/modes/components/status-line/types.ts +1 -1
  181. package/src/modes/components/status-line.ts +4 -171
  182. package/src/modes/components/tool-execution.ts +47 -6
  183. package/src/modes/components/user-message.ts +26 -9
  184. package/src/modes/controllers/command-controller.ts +28 -2
  185. package/src/modes/controllers/event-controller.ts +159 -36
  186. package/src/modes/controllers/extension-ui-controller.ts +37 -18
  187. package/src/modes/controllers/input-controller.ts +8 -2
  188. package/src/modes/controllers/selector-controller.ts +62 -15
  189. package/src/modes/interactive-mode.ts +199 -7
  190. package/src/modes/irc-observation-ledger.ts +127 -0
  191. package/src/modes/print-mode.ts +228 -92
  192. package/src/modes/rpc/rpc-client.ts +58 -1
  193. package/src/modes/rpc/rpc-mode.ts +286 -85
  194. package/src/modes/rpc/rpc-types.ts +15 -1
  195. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  196. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  197. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  198. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  199. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  200. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  201. package/src/modes/types.ts +34 -2
  202. package/src/modes/utils/context-usage.ts +56 -92
  203. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  204. package/src/modes/utils/irc-message.ts +138 -0
  205. package/src/modes/utils/ui-helpers.ts +216 -50
  206. package/src/notifications/config.ts +44 -0
  207. package/src/notifications/daemon-paths.ts +8 -0
  208. package/src/notifications/index.ts +535 -75
  209. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  210. package/src/notifications/notification-service.ts +710 -0
  211. package/src/notifications/rate-limit-pool.ts +46 -12
  212. package/src/notifications/telegram-daemon-control.ts +27 -2
  213. package/src/notifications/telegram-daemon.ts +635 -94
  214. package/src/notifications/telegram-reference.ts +73 -13
  215. package/src/notifications/topic-registry.ts +86 -9
  216. package/src/prompts/system/irc-peer-roster.md +1 -0
  217. package/src/prompts/system/subagent-system-prompt.md +1 -0
  218. package/src/prompts/system/system-prompt.md +1 -0
  219. package/src/prompts/tools/irc.md +1 -1
  220. package/src/registry/agent-registry.ts +3 -0
  221. package/src/sdk.ts +52 -1
  222. package/src/secrets/obfuscator.ts +4 -2
  223. package/src/session/agent-session.ts +670 -217
  224. package/src/session/cache-economics.ts +275 -0
  225. package/src/session/context-estimation.ts +91 -0
  226. package/src/session/provider-safety-stop.ts +9 -0
  227. package/src/session/session-dump-format.ts +6 -0
  228. package/src/session/session-manager.ts +809 -39
  229. package/src/session/session-storage.ts +582 -38
  230. package/src/skc-runtime/session-state-sidecar.ts +756 -135
  231. package/src/skc-runtime/tmux-common.ts +26 -7
  232. package/src/skc-runtime/tmux-gc.ts +52 -9
  233. package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
  234. package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
  235. package/src/skc-runtime/tmux-sessions.ts +871 -68
  236. package/src/slash-commands/builtin-registry.ts +153 -9
  237. package/src/slash-commands/helpers/context-report.ts +26 -4
  238. package/src/task/executor.ts +12 -3
  239. package/src/task/index.ts +8 -0
  240. package/src/task/receipt.ts +7 -2
  241. package/src/task/types.ts +26 -0
  242. package/src/tools/ask-answer-registry.ts +25 -0
  243. package/src/tools/browser/tab-supervisor.ts +63 -6
  244. package/src/tools/browser.ts +8 -3
  245. package/src/tools/index.ts +81 -6
  246. package/src/tools/sqlite-reader.ts +8 -4
  247. package/src/tui/output-block.ts +18 -6
  248. package/src/utils/external-editor.ts +6 -1
@@ -14,22 +14,38 @@ import {
14
14
  import { formatBytes } from "@sayknow-cli/utils";
15
15
  import { theme } from "../../modes/theme/theme";
16
16
  import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
17
- import type { SessionInfo } from "../../session/session-manager";
17
+ import type { ResumeSessionIdentity, ResumeTailInspection, SessionInfo } from "../../session/session-manager";
18
18
  import { DynamicBorder } from "./dynamic-border";
19
19
  import { HookSelectorComponent } from "./hook-selector";
20
20
 
21
- /**
22
- * Custom session list component with multi-line items and search
23
- */
21
+ export interface SelectedSession {
22
+ kind: "selected";
23
+ path: string;
24
+ identity: ResumeSessionIdentity;
25
+ action: "continue-tail" | "open-idle";
26
+ }
27
+
28
+ export interface CancelledSessionSelection {
29
+ kind: "cancelled";
30
+ }
31
+
32
+ export type SessionSelectionResult = SelectedSession | CancelledSessionSelection;
33
+ export type SessionInspector = (sessionPath: string) => Promise<ResumeTailInspection>;
34
+
35
+ type SelectorState =
36
+ | { kind: "browsing" }
37
+ | { kind: "checking"; session: SessionInfo; token: number }
38
+ | { kind: "confirming"; session: SessionInfo; token: number; identity: ResumeSessionIdentity }
39
+ | { kind: "settled" };
40
+
24
41
  class SessionList implements Component {
25
42
  #filteredSessions: SessionInfo[] = [];
26
- #selectedIndex: number = 0;
27
- readonly #searchInput: Input;
28
- onSelect?: (sessionPath: string) => void;
43
+ #selectedIndex = 0;
44
+ readonly #searchInput = new Input();
45
+ #inputFrozen = false;
46
+ onSelect?: (session: SessionInfo) => void;
29
47
  onCancel?: () => void;
30
48
  onExit: () => void = () => {};
31
- #maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)
32
-
33
49
  onDeleteRequest?: (session: SessionInfo) => void;
34
50
 
35
51
  constructor(
@@ -37,255 +53,173 @@ class SessionList implements Component {
37
53
  private readonly showCwd = false,
38
54
  ) {
39
55
  this.#filteredSessions = allSessions;
40
- this.#searchInput = new Input();
41
-
42
- // Handle Enter in search input - select current item
43
56
  this.#searchInput.onSubmit = () => {
44
- if (this.#filteredSessions[this.#selectedIndex]) {
45
- const selected = this.#filteredSessions[this.#selectedIndex];
46
- if (this.onSelect) {
47
- this.onSelect(selected.path);
48
- }
49
- }
57
+ const selected = this.#filteredSessions[this.#selectedIndex];
58
+ if (selected) this.onSelect?.(selected);
50
59
  };
51
60
  }
52
61
 
62
+ setInputFrozen(frozen: boolean): void {
63
+ this.#inputFrozen = frozen;
64
+ }
65
+
53
66
  #clampSelectedIndex(): void {
54
- if (this.#filteredSessions.length === 0) {
55
- this.#selectedIndex = 0;
56
- return;
57
- }
58
- this.#selectedIndex = Math.max(0, Math.min(this.#selectedIndex, this.#filteredSessions.length - 1));
67
+ this.#selectedIndex =
68
+ this.#filteredSessions.length === 0
69
+ ? 0
70
+ : Math.max(0, Math.min(this.#selectedIndex, this.#filteredSessions.length - 1));
59
71
  }
60
72
 
61
73
  #filterSessions(query: string): void {
62
- this.#filteredSessions = fuzzyFilter(this.allSessions, query, session => {
63
- const parts = [
74
+ this.#filteredSessions = fuzzyFilter(this.allSessions, query, session =>
75
+ [
64
76
  session.id,
65
77
  session.title ?? "",
66
78
  session.cwd ?? "",
67
79
  session.firstMessage ?? "",
68
80
  session.allMessagesText,
69
81
  session.path,
70
- ];
71
- return parts.filter(Boolean).join(" ");
72
- });
82
+ ]
83
+ .filter(Boolean)
84
+ .join(" "),
85
+ );
73
86
  this.#clampSelectedIndex();
74
87
  }
75
88
 
76
89
  removeSession(sessionPath: string): void {
77
- const index = this.allSessions.findIndex(s => s.path === sessionPath);
90
+ const index = this.allSessions.findIndex(session => session.path === sessionPath);
78
91
  if (index === -1) return;
79
92
  this.allSessions.splice(index, 1);
80
- // Re-filter to update filteredSessions
81
93
  this.#filterSessions(this.#searchInput.getValue());
82
94
  }
83
95
 
84
- invalidate(): void {
85
- // No cached state to invalidate currently
86
- }
96
+ invalidate(): void {}
87
97
 
88
98
  render(width: number): string[] {
89
- const lines: string[] = [];
90
-
91
- // Render search input
92
- lines.push(...this.#searchInput.render(width));
93
- lines.push(""); // Blank line after search
94
-
99
+ const lines = [...this.#searchInput.render(width), ""];
95
100
  if (this.#filteredSessions.length === 0) {
96
- if (this.showCwd) {
97
- // "All" scope - no sessions anywhere that match filter
98
- lines.push(truncateToWidth(theme.fg("muted", " No sessions found"), width));
99
- } else {
100
- // "Current folder" scope - hint to try "all"
101
- lines.push(
102
- truncateToWidth(theme.fg("muted", " No sessions in current folder. Press Tab to view all."), width),
103
- );
104
- }
101
+ lines.push(
102
+ truncateToWidth(
103
+ theme.fg(
104
+ "muted",
105
+ this.showCwd ? " No sessions found" : " No sessions in current folder. Press Tab to view all.",
106
+ ),
107
+ width,
108
+ ),
109
+ );
105
110
  return lines;
106
111
  }
107
-
108
- // Format dates
109
112
  const formatDate = (date: Date): string => {
110
- const now = new Date();
111
- const diffMs = now.getTime() - date.getTime();
112
- const diffMins = Math.floor(diffMs / 60000);
113
- const diffHours = Math.floor(diffMs / 3600000);
114
- const diffDays = Math.floor(diffMs / 86400000);
115
-
116
- if (diffMins < 1) return "just now";
117
- if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? "s" : ""} ago`;
118
- if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`;
119
- if (diffDays === 1) return "1 day ago";
120
- if (diffDays < 7) return `${diffDays} days ago`;
121
-
122
- return date.toLocaleDateString();
113
+ const minutes = Math.floor((Date.now() - date.getTime()) / 60000);
114
+ if (minutes < 1) return "just now";
115
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
116
+ const hours = Math.floor(minutes / 60);
117
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
118
+ const days = Math.floor(hours / 24);
119
+ return days === 1 ? "1 day ago" : days < 7 ? `${days} days ago` : date.toLocaleDateString();
123
120
  };
124
-
125
- // Calculate visible range with scrolling
126
- const startIndex = Math.max(
121
+ const maxVisible = 5;
122
+ const start = Math.max(
127
123
  0,
128
- Math.min(
129
- this.#selectedIndex - Math.floor(this.#maxVisible / 2),
130
- this.#filteredSessions.length - this.#maxVisible,
131
- ),
124
+ Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), this.#filteredSessions.length - maxVisible),
132
125
  );
133
- const endIndex = Math.min(startIndex + this.#maxVisible, this.#filteredSessions.length);
134
-
135
- // Render visible sessions (2-3 lines per session + blank line)
136
- for (let i = startIndex; i < endIndex; i++) {
137
- const session = this.#filteredSessions[i];
138
- const isSelected = i === this.#selectedIndex;
139
-
140
- // Normalize first message to single line
141
- const normalizedMessage = session.firstMessage.replace(/\n/g, " ").trim();
142
-
143
- // First line: cursor + title (or first message if no title)
144
- const cursorSymbol = `${theme.nav.cursor} `;
145
- const cursorWidth = visibleWidth(cursorSymbol);
146
- const cursor = isSelected ? theme.fg("accent", cursorSymbol) : padding(cursorWidth);
147
- const maxWidth = width - cursorWidth; // Account for cursor width
148
-
149
- if (session.title) {
150
- // Has title: show title on first line, dimmed first message on second line
151
- const truncatedTitle = truncateToWidth(session.title, maxWidth);
152
- const titleLine = cursor + (isSelected ? theme.bold(truncatedTitle) : truncatedTitle);
153
- lines.push(titleLine);
154
-
155
- // Second line: dimmed first message preview
156
- const truncatedPreview = truncateToWidth(normalizedMessage, maxWidth);
157
- lines.push(` ${theme.fg("dim", truncatedPreview)}`);
158
- } else {
159
- // No title: show first message as main line
160
- const truncatedMsg = truncateToWidth(normalizedMessage, maxWidth);
161
- const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
162
- lines.push(messageLine);
163
- }
164
-
165
- // Metadata line: date + file size
166
- const modified = formatDate(session.modified);
167
- const metadata = ` ${modified} ${theme.sep.dot} ${formatBytes(session.size)}`;
168
- const metadataLine = theme.fg("dim", truncateToWidth(metadata, width));
169
-
170
- lines.push(metadataLine);
171
- lines.push(""); // Blank line between sessions
172
- }
173
-
174
- // Add scroll indicator if needed
175
- if (startIndex > 0 || endIndex < this.#filteredSessions.length) {
176
- const scrollText = ` (${this.#selectedIndex + 1}/${this.#filteredSessions.length})`;
177
- const scrollInfo = theme.fg("muted", truncateToWidth(scrollText, width));
178
- lines.push(scrollInfo);
126
+ const end = Math.min(start + maxVisible, this.#filteredSessions.length);
127
+ for (let index = start; index < end; index++) {
128
+ const session = this.#filteredSessions[index];
129
+ const selected = index === this.#selectedIndex;
130
+ const cursor = `${theme.nav.cursor} `;
131
+ const cursorWidth = visibleWidth(cursor);
132
+ const prefix = selected ? theme.fg("accent", cursor) : padding(cursorWidth);
133
+ const message = session.firstMessage.replace(/\n/g, " ").trim();
134
+ const title = truncateToWidth(session.title ?? message, width - cursorWidth);
135
+ lines.push(prefix + (selected ? theme.bold(title) : title));
136
+ if (session.title) lines.push(` ${theme.fg("dim", truncateToWidth(message, width - cursorWidth))}`);
137
+ lines.push(
138
+ theme.fg(
139
+ "dim",
140
+ truncateToWidth(
141
+ ` ${formatDate(session.modified)} ${theme.sep.dot} ${formatBytes(session.size)}`,
142
+ width,
143
+ ),
144
+ ),
145
+ );
146
+ lines.push("");
179
147
  }
180
-
181
- // Add keybinding hint
182
- lines.push("");
183
- lines.push(theme.fg("muted", " [Del to delete selected transcript/artifacts, Enter to select, Esc to cancel]"));
184
-
148
+ if (start > 0 || end < this.#filteredSessions.length)
149
+ lines.push(theme.fg("muted", ` (${this.#selectedIndex + 1}/${this.#filteredSessions.length})`));
150
+ lines.push(
151
+ "",
152
+ theme.fg("muted", " [Del to delete selected transcript/artifacts, Enter to select, Esc to cancel]"),
153
+ );
185
154
  return lines;
186
155
  }
187
156
 
188
157
  handleInput(keyData: string): void {
189
- const moveSelection = (delta: number): void => {
158
+ if (matchesAppInterrupt(keyData)) {
159
+ this.onCancel?.();
160
+ return;
161
+ }
162
+ if (matchesKey(keyData, "ctrl+c")) {
163
+ this.onExit();
164
+ return;
165
+ }
166
+ if (this.#inputFrozen) return;
167
+ const move = (delta: number): void => {
190
168
  if (this.#filteredSessions.length === 0) return;
191
169
  this.#selectedIndex += delta;
192
170
  this.#clampSelectedIndex();
193
171
  };
194
-
195
- // Delete key - request delete confirmation from parent
196
172
  if (matchesKey(keyData, "delete")) {
197
173
  const selected = this.#filteredSessions[this.#selectedIndex];
198
- if (selected && this.onDeleteRequest) {
199
- this.onDeleteRequest(selected);
200
- }
201
- return;
202
- }
203
-
204
- // Up arrow
205
- if (matchesKey(keyData, "up")) {
206
- moveSelection(-1);
207
- return;
208
- }
209
- // Down arrow
210
- if (matchesKey(keyData, "down")) {
211
- moveSelection(1);
212
- return;
213
- }
214
- // Page up - jump up by maxVisible items
215
- if (matchesKey(keyData, "pageUp")) {
216
- moveSelection(-this.#maxVisible);
217
- return;
218
- }
219
- // Page down - jump down by maxVisible items
220
- if (matchesKey(keyData, "pageDown")) {
221
- moveSelection(this.#maxVisible);
222
- return;
223
- }
224
- // Enter
225
- if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
174
+ if (selected) this.onDeleteRequest?.(selected);
175
+ } else if (matchesKey(keyData, "up")) move(-1);
176
+ else if (matchesKey(keyData, "down")) move(1);
177
+ else if (matchesKey(keyData, "pageUp")) move(-5);
178
+ else if (matchesKey(keyData, "pageDown")) move(5);
179
+ else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
226
180
  const selected = this.#filteredSessions[this.#selectedIndex];
227
- if (selected && this.onSelect) {
228
- this.onSelect(selected.path);
229
- }
230
- return;
231
- }
232
- // Escape - cancel
233
- if (matchesAppInterrupt(keyData)) {
234
- if (this.onCancel) {
235
- this.onCancel();
236
- }
237
- return;
238
- }
239
- // Ctrl+C - exit
240
- if (matchesKey(keyData, "ctrl+c")) {
241
- this.onExit();
242
- return;
181
+ if (selected) this.onSelect?.(selected);
182
+ } else {
183
+ this.#searchInput.handleInput(keyData);
184
+ this.#filterSessions(this.#searchInput.getValue());
243
185
  }
244
- // Pass everything else to search input
245
- this.#searchInput.handleInput(keyData);
246
- this.#filterSessions(this.#searchInput.getValue());
247
186
  }
248
187
  }
249
188
 
250
- /**
251
- * Component that renders a session selector with optional confirmation dialog
252
- */
189
+ /** A one-shot resume consent selector. It never opens or mutates sessions. */
253
190
  export class SessionSelectorComponent extends Container {
254
191
  #sessionList: SessionList;
255
192
  #confirmationDialog: HookSelectorComponent | null = null;
256
- #messageContainer: Container;
257
- #onDelete?: (session: SessionInfo) => Promise<boolean>;
193
+ #messageContainer = new Container();
258
194
  #onRequestRender?: () => void;
195
+ #state: SelectorState = { kind: "browsing" };
196
+ #nextToken = 0;
259
197
 
260
198
  constructor(
261
199
  sessions: SessionInfo[],
262
- onSelect: (sessionPath: string) => void,
263
- onCancel: () => void,
264
- onExit: () => void,
265
- onDelete?: (session: SessionInfo) => Promise<boolean>,
200
+ private readonly onSelect: (sessionPath: string) => void,
201
+ private readonly onCancel: () => void,
202
+ private readonly onExit: () => void,
203
+ private readonly onDelete?: (session: SessionInfo) => Promise<boolean>,
204
+ private readonly inspector?: SessionInspector,
205
+ private readonly onSelection?: (selection: SessionSelectionResult) => void,
266
206
  ) {
267
207
  super();
268
-
269
- this.#messageContainer = new Container();
270
- this.#onDelete = onDelete;
271
- // Add header
272
208
  this.addChild(new Spacer(1));
273
209
  this.addChild(new Text(theme.bold("Resume Session"), 1, 0));
274
210
  this.addChild(new Spacer(1));
275
211
  this.addChild(new DynamicBorder());
276
212
  this.addChild(new Spacer(1));
277
213
  this.addChild(this.#messageContainer);
278
- // Create session list
279
214
  this.#sessionList = new SessionList(sessions);
280
- this.#sessionList.onSelect = onSelect;
281
- this.#sessionList.onCancel = onCancel;
282
- this.#sessionList.onExit = onExit;
283
- this.#sessionList.onDeleteRequest = (session: SessionInfo) => {
284
- this.#showDeleteConfirmation(session);
215
+ this.#sessionList.onSelect = session => {
216
+ if (this.inspector) void this.#inspect(session);
217
+ else this.onSelect(session.path);
285
218
  };
219
+ this.#sessionList.onCancel = () => this.#cancel();
220
+ this.#sessionList.onExit = () => this.#exit();
221
+ this.#sessionList.onDeleteRequest = session => this.#showDeleteConfirmation(session);
286
222
  this.addChild(this.#sessionList);
287
-
288
- // Add bottom border
289
223
  this.addChild(new Spacer(1));
290
224
  this.addChild(new DynamicBorder());
291
225
  }
@@ -293,60 +227,127 @@ export class SessionSelectorComponent extends Container {
293
227
  setOnRequestRender(callback: () => void): void {
294
228
  this.#onRequestRender = callback;
295
229
  }
296
-
297
230
  #clearError(): void {
298
231
  this.#messageContainer.clear();
299
232
  }
300
-
301
233
  #showError(message: string): void {
302
234
  this.#messageContainer.clear();
303
- this.#messageContainer.addChild(new Text(theme.fg("error", `Error: ${replaceTabs(message)}`), 1, 0));
235
+ this.#messageContainer.addChild(
236
+ new Text(theme.fg("error", `Error: ${replaceTabs(message).slice(0, 200)}`), 1, 0),
237
+ );
304
238
  this.#messageContainer.addChild(new Spacer(1));
305
239
  }
240
+ #requestRender(): void {
241
+ this.#onRequestRender?.();
242
+ }
306
243
 
244
+ #settle(selection: SessionSelectionResult): void {
245
+ if (this.#state.kind === "settled") return;
246
+ this.#state = { kind: "settled" };
247
+ this.#sessionList.setInputFrozen(true);
248
+ const dialog = this.#confirmationDialog;
249
+ this.#confirmationDialog = null;
250
+ if (dialog) this.removeChild(dialog);
251
+ if (this.onSelection) this.onSelection(selection);
252
+ else if (selection.kind === "selected") this.onSelect(selection.path);
253
+ else this.onCancel();
254
+ }
255
+ #cancel(): void {
256
+ if (this.#state.kind === "settled") return;
257
+ if (this.#state.kind === "checking") this.#nextToken++;
258
+ this.#settle({ kind: "cancelled" });
259
+ }
260
+ #exit(): void {
261
+ if (this.#state.kind !== "settled") this.#nextToken++;
262
+ this.#state = { kind: "settled" };
263
+ this.#sessionList.setInputFrozen(true);
264
+ this.onExit();
265
+ }
266
+ async #inspect(session: SessionInfo): Promise<void> {
267
+ const inspector = this.inspector;
268
+ if (!inspector || this.#state.kind !== "browsing") return;
269
+ const token = ++this.#nextToken;
270
+ this.#state = { kind: "checking", session, token };
271
+ this.#sessionList.setInputFrozen(true);
272
+ this.#clearError();
273
+ this.#requestRender();
274
+ try {
275
+ const inspection = await inspector(session.path);
276
+ if (this.#state.kind !== "checking" || this.#state.token !== token) return;
277
+ if (inspection.kind === "error") {
278
+ this.#state = { kind: "browsing" };
279
+ this.#sessionList.setInputFrozen(false);
280
+ this.#showError(`Unable to inspect session (${inspection.reason}).`);
281
+ this.#requestRender();
282
+ return;
283
+ }
284
+ if (inspection.kind === "terminal") {
285
+ this.#settle({ kind: "selected", path: session.path, identity: inspection.identity, action: "open-idle" });
286
+ return;
287
+ }
288
+ this.#showResumeConfirmation(session, token, inspection.identity);
289
+ } catch (error) {
290
+ if (this.#state.kind !== "checking" || this.#state.token !== token) return;
291
+ this.#state = { kind: "browsing" };
292
+ this.#sessionList.setInputFrozen(false);
293
+ this.#showError(error instanceof Error ? error.message : String(error));
294
+ this.#requestRender();
295
+ }
296
+ }
297
+ #showResumeConfirmation(session: SessionInfo, token: number, identity: ResumeSessionIdentity): void {
298
+ this.#state = { kind: "confirming", session, token, identity };
299
+ const dialog = new HookSelectorComponent(
300
+ "Resume this session?",
301
+ ["Yes", "No"],
302
+ option => {
303
+ if (this.#confirmationDialog !== dialog || this.#state.kind !== "confirming") return;
304
+ if (option === "Yes")
305
+ this.#settle({ kind: "selected", path: session.path, identity, action: "continue-tail" });
306
+ else this.#settle({ kind: "cancelled" });
307
+ },
308
+ () => this.#settle({ kind: "cancelled" }),
309
+ { acceleratorMap: { y: "Yes", n: "No" } },
310
+ );
311
+ this.#confirmationDialog = dialog;
312
+ this.addChild(dialog);
313
+ this.#requestRender();
314
+ }
307
315
  #showDeleteConfirmation(session: SessionInfo): void {
316
+ if (this.#state.kind !== "browsing" || this.#confirmationDialog) return;
308
317
  const displayName = session.title || session.firstMessage.slice(0, 40) || session.id;
309
- this.#confirmationDialog = new HookSelectorComponent(
318
+ let deleting = false;
319
+ const dialog = new HookSelectorComponent(
310
320
  `Delete selected session transcript and artifacts?\n${displayName}\nThis cannot be undone. Other sessions and topic/history metadata are not deleted.`,
311
321
  ["Yes", "No"],
312
- async (option: string) => {
313
- if (option === "Yes" && this.#onDelete) {
322
+ async option => {
323
+ if (this.#confirmationDialog !== dialog || deleting) return;
324
+ if (option === "Yes" && this.onDelete) {
325
+ deleting = true;
314
326
  this.#clearError();
315
327
  try {
316
- const deleted = await this.#onDelete(session);
317
- if (deleted) {
318
- this.#sessionList.removeSession(session.path);
319
- }
320
- } catch (err) {
321
- this.#showError(err instanceof Error ? err.message : String(err));
328
+ if (await this.onDelete(session)) this.#sessionList.removeSession(session.path);
329
+ } catch (error) {
330
+ this.#showError(error instanceof Error ? error.message : String(error));
322
331
  }
323
332
  }
324
- // Close confirmation dialog
325
- this.removeChild(this.#confirmationDialog!);
333
+ this.removeChild(dialog);
326
334
  this.#confirmationDialog = null;
327
- // Request rerender
328
- this.#onRequestRender?.();
335
+ this.#requestRender();
329
336
  },
330
337
  () => {
331
- // Cancel - close confirmation dialog
332
- this.removeChild(this.#confirmationDialog!);
338
+ if (this.#confirmationDialog !== dialog || deleting) return;
339
+ this.removeChild(dialog);
333
340
  this.#confirmationDialog = null;
334
- // Request rerender
335
- this.#onRequestRender?.();
341
+ this.#requestRender();
336
342
  },
337
343
  );
338
- // Show confirmation dialog
339
- this.addChild(this.#confirmationDialog);
344
+ this.#confirmationDialog = dialog;
345
+ this.addChild(dialog);
340
346
  }
341
-
342
347
  handleInput(keyData: string): void {
343
- if (this.#confirmationDialog) {
344
- this.#confirmationDialog.handleInput(keyData);
345
- } else {
346
- this.#sessionList.handleInput(keyData);
347
- }
348
+ if (this.#confirmationDialog) this.#confirmationDialog.handleInput(keyData);
349
+ else this.#sessionList.handleInput(keyData);
348
350
  }
349
-
350
351
  getSessionList(): SessionList {
351
352
  return this.#sessionList;
352
353
  }
@@ -694,6 +694,8 @@ export interface SettingsCallbacks {
694
694
  onThemePreview?: (theme: string) => void | Promise<void>;
695
695
  /** Called to restore the rendered theme when theme settings preview is cancelled */
696
696
  onThemePreviewCancel?: (theme: string) => void | Promise<void>;
697
+ /** Called to live-preview the sayknow pet skin while browsing the pet setting. */
698
+ onPetPreview?: (mode: string) => void;
697
699
  /** Called for status line preview while configuring */
698
700
  onStatusLinePreview?: (settings: StatusLinePreviewSettings) => void;
699
701
  /** Get current rendered status line for inline preview */
@@ -939,6 +941,14 @@ export class SettingsSelectorComponent extends Container {
939
941
  });
940
942
  this.#updateStatusPreview();
941
943
  };
944
+ } else if (def.path === "pet.mode") {
945
+ const savedPetMode = currentValue;
946
+ onPreview = value => {
947
+ this.callbacks.onPetPreview?.(value);
948
+ };
949
+ onPreviewCancel = () => {
950
+ this.callbacks.onPetPreview?.(savedPetMode);
951
+ };
942
952
  }
943
953
 
944
954
  // Provide status line preview for theme selection
@@ -115,9 +115,13 @@ const modelSegment: StatusLineSegment = {
115
115
  if (opts.showContextPercent !== false && ctx.contextPctSegmentActive !== true) {
116
116
  const pct = ctx.contextPercent;
117
117
  const window = ctx.contextWindow;
118
- if (window > 0 && Number.isFinite(pct)) {
119
- const color = getContextUsageThemeColor(getContextUsageLevel(pct, window));
120
- content += `${theme.sep.dot}${theme.fg(color, `${pct.toFixed(1)}%`)}`;
118
+ if (window > 0) {
119
+ if (typeof pct === "number" && Number.isFinite(pct)) {
120
+ const color = getContextUsageThemeColor(getContextUsageLevel(pct, window));
121
+ content += `${theme.sep.dot}${theme.fg(color, `${pct.toFixed(1)}%`)}`;
122
+ } else {
123
+ content += `${theme.sep.dot}${theme.fg("statusLineContext", "?")}`;
124
+ }
121
125
  }
122
126
  }
123
127
 
@@ -383,11 +387,14 @@ const contextPctSegment: StatusLineSegment = {
383
387
  render(ctx) {
384
388
  const pct = ctx.contextPercent;
385
389
  const window = ctx.contextWindow;
390
+ const knownPct = typeof pct === "number" && Number.isFinite(pct) ? pct : undefined;
386
391
 
387
392
  const autoIcon = ctx.autoCompactEnabled && theme.icon.auto ? ` ${theme.icon.auto}` : "";
388
- const text = `${pct.toFixed(1)}%/${formatNumber(window)}${autoIcon}`;
389
-
390
- const color = getContextUsageThemeColor(getContextUsageLevel(pct, window));
393
+ const text = `${knownPct === undefined ? "?" : `${knownPct.toFixed(1)}%`}/${formatNumber(window)}${autoIcon}`;
394
+ const color =
395
+ knownPct === undefined
396
+ ? "statusLineContext"
397
+ : getContextUsageThemeColor(getContextUsageLevel(knownPct, window));
391
398
  const content = withIcon(theme.icon.context, theme.fg(color, text));
392
399
 
393
400
  return { content, visible: true };
@@ -399,7 +406,7 @@ const contextBarSegment: StatusLineSegment = {
399
406
  render(ctx) {
400
407
  const pct = ctx.contextPercent;
401
408
  const window = ctx.contextWindow;
402
- if (!window) return { content: "", visible: false };
409
+ if (!window || pct === null) return { content: "", visible: false };
403
410
  const width = 10;
404
411
  const filled = Math.min(width, Math.max(0, Math.round((pct / 100) * width)));
405
412
  const bar = "█".repeat(filled) + "░".repeat(width - filled);
@@ -39,7 +39,7 @@ export interface SegmentContext {
39
39
  cost: number;
40
40
  tokensPerSecond: number | null;
41
41
  };
42
- contextPercent: number;
42
+ contextPercent: number | null;
43
43
  contextWindow: number;
44
44
  /**
45
45
  * True when a standalone `context_pct` segment is also part of the active