@sayknow-cli/coding-agent 0.6.4 → 0.6.5

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 (81) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +2 -2
  3. package/dist/types/config/keybindings.d.ts +1 -1
  4. package/dist/types/config/settings-schema.d.ts +7 -3
  5. package/dist/types/i18n/messages/en.d.ts +16 -0
  6. package/dist/types/modes/components/session-selector.d.ts +7 -3
  7. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  8. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  9. package/dist/types/modes/components/user-message.d.ts +3 -1
  10. package/dist/types/modes/components/welcome.d.ts +49 -6
  11. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  12. package/dist/types/modes/controllers/selector-controller.d.ts +5 -0
  13. package/dist/types/modes/interactive-mode.d.ts +7 -1
  14. package/dist/types/modes/theme/defaults/index.d.ts +342 -0
  15. package/dist/types/modes/theme/theme.d.ts +5 -1
  16. package/dist/types/modes/types.d.ts +1 -1
  17. package/dist/types/session/session-manager.d.ts +9 -0
  18. package/dist/types/session/session-stars.d.ts +15 -0
  19. package/dist/types/tui/output-block.d.ts +12 -1
  20. package/dist/types/tui/utils.d.ts +1 -3
  21. package/dist/types/utils/session-title-generation.d.ts +6 -0
  22. package/dist/types/utils/title-generator.d.ts +11 -2
  23. package/package.json +7 -7
  24. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  25. package/src/cli/session-picker.ts +4 -0
  26. package/src/config/keybindings.ts +1 -1
  27. package/src/config/settings-schema.ts +4 -3
  28. package/src/i18n/messages/de.messages.ts +4 -1
  29. package/src/i18n/messages/de.settings.ts +2 -0
  30. package/src/i18n/messages/de.ts +16 -0
  31. package/src/i18n/messages/en.ts +16 -0
  32. package/src/i18n/messages/es.messages.ts +4 -1
  33. package/src/i18n/messages/es.settings.ts +2 -0
  34. package/src/i18n/messages/es.ts +16 -0
  35. package/src/i18n/messages/fr.messages.ts +4 -1
  36. package/src/i18n/messages/fr.settings.ts +2 -0
  37. package/src/i18n/messages/fr.ts +16 -0
  38. package/src/i18n/messages/ja.messages.ts +4 -1
  39. package/src/i18n/messages/ja.settings.ts +2 -0
  40. package/src/i18n/messages/ja.ts +16 -0
  41. package/src/i18n/messages/ko.messages.ts +4 -1
  42. package/src/i18n/messages/ko.settings.ts +2 -0
  43. package/src/i18n/messages/ko.ts +16 -0
  44. package/src/i18n/messages/zh.messages.ts +2 -1
  45. package/src/i18n/messages/zh.settings.ts +2 -0
  46. package/src/i18n/messages/zh.ts +16 -0
  47. package/src/internal-urls/docs-index.generated.ts +10 -10
  48. package/src/lsp/render.ts +0 -1
  49. package/src/modes/components/session-selector.ts +71 -21
  50. package/src/modes/components/sessions-dashboard.ts +8 -5
  51. package/src/modes/components/settings-selector.ts +1 -0
  52. package/src/modes/components/status-line/presets.ts +5 -5
  53. package/src/modes/components/status-line/separators.ts +4 -0
  54. package/src/modes/components/tool-execution.ts +6 -19
  55. package/src/modes/components/tool-status-header.ts +103 -12
  56. package/src/modes/components/user-message-selector.ts +57 -15
  57. package/src/modes/components/user-message.ts +25 -15
  58. package/src/modes/components/welcome.ts +374 -478
  59. package/src/modes/controllers/command-controller.ts +56 -7
  60. package/src/modes/controllers/input-controller.ts +4 -0
  61. package/src/modes/controllers/selector-controller.ts +91 -10
  62. package/src/modes/interactive-mode.ts +101 -12
  63. package/src/modes/theme/defaults/glow-octopus.json +114 -0
  64. package/src/modes/theme/defaults/index.ts +6 -0
  65. package/src/modes/theme/defaults/ink-octopus.json +114 -0
  66. package/src/modes/theme/defaults/violet-octopus.json +114 -0
  67. package/src/modes/theme/theme.ts +18 -2
  68. package/src/modes/types.ts +1 -1
  69. package/src/prompts/system/title-system.md +1 -1
  70. package/src/sdk/protocol/operation-inventory.generated.json +40 -0
  71. package/src/session/agent-session.ts +12 -0
  72. package/src/session/internal/managed-session-scope.ts +10 -0
  73. package/src/session/session-manager.ts +26 -1
  74. package/src/session/session-stars.ts +104 -0
  75. package/src/slash-commands/builtin-registry.ts +104 -4
  76. package/src/tools/debug.ts +0 -1
  77. package/src/tools/fetch.ts +0 -1
  78. package/src/tui/output-block.ts +29 -61
  79. package/src/tui/utils.ts +1 -8
  80. package/src/utils/session-title-generation.ts +29 -0
  81. package/src/utils/title-generator.ts +46 -2
@@ -0,0 +1,114 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/jaybeyond/Sayknow_CLI/main/packages/coding-agent/theme-schema.json",
3
+ "name": "ink-octopus",
4
+ "vars": {
5
+ "ink": "#14110e",
6
+ "mantle": "#1b1712",
7
+ "surface": "#241e18",
8
+ "surfaceBright": "#322a21",
9
+ "text": "#ece4d6",
10
+ "muted": "#b3a794",
11
+ "dim": "#7d7264",
12
+ "accent": "#e8a23a",
13
+ "secondary": "#d4bd95",
14
+ "success": "#9fbf7a",
15
+ "error": "#e2664f",
16
+ "warning": "#f0c75e",
17
+ "link": "#e6b77c",
18
+ "keyword": "#e8a23a",
19
+ "function": "#e6cf9c",
20
+ "string": "#a9c58a",
21
+ "number": "#e59f78",
22
+ "type": "#d9b2d6",
23
+ "diffRemoved": "#d9695a",
24
+ "toolSuccessBg": "#1d2317",
25
+ "toolErrorBg": "#2d1915",
26
+ "think1": "#c9b28f",
27
+ "think2": "#e0b27a",
28
+ "think3": "#e8a23a",
29
+ "think4": "#e2664f"
30
+ },
31
+ "colors": {
32
+ "accent": "accent",
33
+ "border": "surfaceBright",
34
+ "borderAccent": "accent",
35
+ "borderMuted": "dim",
36
+ "success": "success",
37
+ "error": "error",
38
+ "warning": "warning",
39
+ "muted": "muted",
40
+ "dim": "dim",
41
+ "text": "text",
42
+ "thinkingText": "muted",
43
+ "selectedBg": "surfaceBright",
44
+ "userMessageBg": "surface",
45
+ "userMessageText": "text",
46
+ "customMessageBg": "mantle",
47
+ "customMessageText": "text",
48
+ "customMessageLabel": "accent",
49
+ "toolPendingBg": "mantle",
50
+ "toolSuccessBg": "toolSuccessBg",
51
+ "toolErrorBg": "toolErrorBg",
52
+ "toolTitle": "text",
53
+ "toolOutput": "muted",
54
+ "mdHeading": "accent",
55
+ "mdLink": "link",
56
+ "mdLinkUrl": "dim",
57
+ "mdCode": "secondary",
58
+ "mdCodeBlock": "string",
59
+ "mdCodeBlockBorder": "surfaceBright",
60
+ "mdQuote": "muted",
61
+ "mdQuoteBorder": "surfaceBright",
62
+ "mdHr": "dim",
63
+ "mdListBullet": "accent",
64
+ "toolDiffAdded": "success",
65
+ "toolDiffRemoved": "diffRemoved",
66
+ "toolDiffContext": "muted",
67
+ "link": "link",
68
+ "syntaxComment": "dim",
69
+ "syntaxKeyword": "keyword",
70
+ "syntaxFunction": "function",
71
+ "syntaxVariable": "text",
72
+ "syntaxString": "string",
73
+ "syntaxNumber": "number",
74
+ "syntaxType": "type",
75
+ "syntaxOperator": "muted",
76
+ "syntaxPunctuation": "dim",
77
+ "thinkingOff": "dim",
78
+ "thinkingMinimal": "muted",
79
+ "thinkingLow": "think1",
80
+ "thinkingMedium": "think2",
81
+ "thinkingHigh": "think3",
82
+ "thinkingXhigh": "think4",
83
+ "bashMode": "accent",
84
+ "statusLineBg": "surfaceBright",
85
+ "statusLineSep": "dim",
86
+ "statusLineModel": "accent",
87
+ "statusLinePath": "secondary",
88
+ "statusLineGitClean": "success",
89
+ "statusLineGitDirty": "warning",
90
+ "statusLineContext": "string",
91
+ "statusLineSpend": "warning",
92
+ "statusLineStaged": "success",
93
+ "statusLineDirty": "warning",
94
+ "statusLineUntracked": "diffRemoved",
95
+ "statusLineOutput": "text",
96
+ "statusLineCost": "accent",
97
+ "statusLineSubagents": "secondary",
98
+ "pythonMode": "function"
99
+ },
100
+ "export": {
101
+ "pageBg": "#14110e",
102
+ "cardBg": "#1b1712",
103
+ "infoBg": "#241e18"
104
+ },
105
+ "symbols": {
106
+ "preset": "unicode",
107
+ "overrides": {
108
+ "icon.pi": "🐙",
109
+ "icon.agents": "🦑",
110
+ "status.running": "◐",
111
+ "md.bullet": "›"
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,114 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/jaybeyond/Sayknow_CLI/main/packages/coding-agent/theme-schema.json",
3
+ "name": "violet-octopus",
4
+ "vars": {
5
+ "ink": "#110c17",
6
+ "mantle": "#191220",
7
+ "surface": "#221829",
8
+ "surfaceBright": "#2e2138",
9
+ "text": "#efe7f7",
10
+ "muted": "#bba9cf",
11
+ "dim": "#7a6a8c",
12
+ "accent": "#b692ff",
13
+ "secondary": "#ffb482",
14
+ "success": "#8fd6a8",
15
+ "error": "#ff6f91",
16
+ "warning": "#ffcf70",
17
+ "link": "#d3b8ff",
18
+ "keyword": "#b692ff",
19
+ "function": "#ffb482",
20
+ "string": "#8fd6a8",
21
+ "number": "#ffcf70",
22
+ "type": "#d3b8ff",
23
+ "diffRemoved": "#ff6f91",
24
+ "toolSuccessBg": "#17241c",
25
+ "toolErrorBg": "#2c1522",
26
+ "think1": "#d3b8ff",
27
+ "think2": "#c5a4ff",
28
+ "think3": "#b692ff",
29
+ "think4": "#ffb482"
30
+ },
31
+ "colors": {
32
+ "accent": "accent",
33
+ "border": "surfaceBright",
34
+ "borderAccent": "accent",
35
+ "borderMuted": "dim",
36
+ "success": "success",
37
+ "error": "error",
38
+ "warning": "warning",
39
+ "muted": "muted",
40
+ "dim": "dim",
41
+ "text": "text",
42
+ "thinkingText": "muted",
43
+ "selectedBg": "surfaceBright",
44
+ "userMessageBg": "surface",
45
+ "userMessageText": "text",
46
+ "customMessageBg": "mantle",
47
+ "customMessageText": "text",
48
+ "customMessageLabel": "accent",
49
+ "toolPendingBg": "mantle",
50
+ "toolSuccessBg": "toolSuccessBg",
51
+ "toolErrorBg": "toolErrorBg",
52
+ "toolTitle": "text",
53
+ "toolOutput": "muted",
54
+ "mdHeading": "accent",
55
+ "mdLink": "link",
56
+ "mdLinkUrl": "dim",
57
+ "mdCode": "secondary",
58
+ "mdCodeBlock": "string",
59
+ "mdCodeBlockBorder": "surfaceBright",
60
+ "mdQuote": "muted",
61
+ "mdQuoteBorder": "surfaceBright",
62
+ "mdHr": "dim",
63
+ "mdListBullet": "accent",
64
+ "toolDiffAdded": "success",
65
+ "toolDiffRemoved": "diffRemoved",
66
+ "toolDiffContext": "muted",
67
+ "link": "link",
68
+ "syntaxComment": "dim",
69
+ "syntaxKeyword": "keyword",
70
+ "syntaxFunction": "function",
71
+ "syntaxVariable": "text",
72
+ "syntaxString": "string",
73
+ "syntaxNumber": "number",
74
+ "syntaxType": "type",
75
+ "syntaxOperator": "muted",
76
+ "syntaxPunctuation": "dim",
77
+ "thinkingOff": "dim",
78
+ "thinkingMinimal": "muted",
79
+ "thinkingLow": "think1",
80
+ "thinkingMedium": "think2",
81
+ "thinkingHigh": "think3",
82
+ "thinkingXhigh": "think4",
83
+ "bashMode": "accent",
84
+ "statusLineBg": "surfaceBright",
85
+ "statusLineSep": "dim",
86
+ "statusLineModel": "accent",
87
+ "statusLinePath": "secondary",
88
+ "statusLineGitClean": "success",
89
+ "statusLineGitDirty": "warning",
90
+ "statusLineContext": "string",
91
+ "statusLineSpend": "warning",
92
+ "statusLineStaged": "success",
93
+ "statusLineDirty": "warning",
94
+ "statusLineUntracked": "diffRemoved",
95
+ "statusLineOutput": "text",
96
+ "statusLineCost": "accent",
97
+ "statusLineSubagents": "secondary",
98
+ "pythonMode": "function"
99
+ },
100
+ "export": {
101
+ "pageBg": "#110c17",
102
+ "cardBg": "#191220",
103
+ "infoBg": "#221829"
104
+ },
105
+ "symbols": {
106
+ "preset": "unicode",
107
+ "overrides": {
108
+ "icon.pi": "🐙",
109
+ "icon.agents": "🦑",
110
+ "status.running": "◐",
111
+ "md.bullet": "▸"
112
+ }
113
+ }
114
+ }
@@ -107,6 +107,7 @@ export type SymbolKey =
107
107
  | "icon.output"
108
108
  | "icon.host"
109
109
  | "icon.session"
110
+ | "icon.star"
110
111
  | "icon.package"
111
112
  | "icon.warning"
112
113
  | "icon.rewind"
@@ -142,6 +143,8 @@ export type SymbolKey =
142
143
  | "md.quoteBorder"
143
144
  | "md.hrChar"
144
145
  | "md.bullet"
146
+ // Transcript rail marking the user's own prompts
147
+ | "rail.user"
145
148
  // Language/file type icons
146
149
  | "lang.default"
147
150
  | "lang.typescript"
@@ -272,6 +275,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
272
275
  "icon.output": "⤴",
273
276
  "icon.host": "🖥",
274
277
  "icon.session": "🆔",
278
+ "icon.star": "★",
275
279
  "icon.package": "📦",
276
280
  "icon.warning": "⚠",
277
281
  "icon.rewind": "↶",
@@ -307,6 +311,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
307
311
  "md.quoteBorder": "▏",
308
312
  "md.hrChar": "─",
309
313
  "md.bullet": "•",
314
+ "rail.user": "▌",
310
315
  // Language/file icons (emoji-centric, no Nerd Font required)
311
316
  "lang.default": "⌘",
312
317
  "lang.typescript": "🟦",
@@ -507,6 +512,7 @@ const NERD_SYMBOLS: SymbolMap = {
507
512
  "icon.host": "\uf109",
508
513
  // pick:  | alt:  
509
514
  "icon.session": "\uf550",
515
+ "icon.star": "\uf005",
510
516
  // pick:  | alt: 
511
517
  "icon.package": "\uf487",
512
518
  // pick:  | alt:  
@@ -567,6 +573,7 @@ const NERD_SYMBOLS: SymbolMap = {
567
573
  "md.hrChar": "─",
568
574
  // pick:  | alt:  •
569
575
  "md.bullet": "\uf111",
576
+ "rail.user": "▌",
570
577
  // Language icons (nerd font devicons)
571
578
  "lang.default": "",
572
579
  "lang.typescript": "\u{E628}",
@@ -696,6 +703,7 @@ const ASCII_SYMBOLS: SymbolMap = {
696
703
  "icon.output": "out:",
697
704
  "icon.host": "host",
698
705
  "icon.session": "id",
706
+ "icon.star": "*",
699
707
  "icon.package": "[P]",
700
708
  "icon.warning": "[!]",
701
709
  "icon.rewind": "<-",
@@ -730,6 +738,7 @@ const ASCII_SYMBOLS: SymbolMap = {
730
738
  "md.quoteBorder": "|",
731
739
  "md.hrChar": "-",
732
740
  "md.bullet": "*",
741
+ "rail.user": "|",
733
742
  // Language icons (ASCII uses abbreviations)
734
743
  "lang.default": "code",
735
744
  "lang.typescript": "ts",
@@ -1471,6 +1480,7 @@ export class Theme {
1471
1480
  output: this.#symbols["icon.output"],
1472
1481
  host: this.#symbols["icon.host"],
1473
1482
  session: this.#symbols["icon.session"],
1483
+ star: this.#symbols["icon.star"],
1474
1484
  package: this.#symbols["icon.package"],
1475
1485
  warning: this.#symbols["icon.warning"],
1476
1486
  rewind: this.#symbols["icon.rewind"],
@@ -1516,6 +1526,12 @@ export class Theme {
1516
1526
  };
1517
1527
  }
1518
1528
 
1529
+ get rail() {
1530
+ return {
1531
+ user: this.#symbols["rail.user"],
1532
+ };
1533
+ }
1534
+
1519
1535
  get md() {
1520
1536
  return {
1521
1537
  quoteBorder: this.#symbols["md.quoteBorder"],
@@ -1785,7 +1801,7 @@ var themeWatcher: fs.FSWatcher | undefined;
1785
1801
  var themeReloadTimer: NodeJS.Timeout | undefined;
1786
1802
  var sigwinchHandler: (() => void) | undefined;
1787
1803
  var autoDetectedTheme: boolean = false;
1788
- var autoDarkTheme: string = "blue-octopus";
1804
+ var autoDarkTheme: string = "ink-octopus";
1789
1805
  var autoLightTheme: string = "blue-octopus";
1790
1806
  var onThemeChangeCallback: (() => void) | undefined;
1791
1807
  var themeLoadRequestId: number = 0;
@@ -1806,7 +1822,7 @@ export async function initTheme(
1806
1822
  lightTheme?: string,
1807
1823
  ): Promise<void> {
1808
1824
  autoDetectedTheme = true;
1809
- autoDarkTheme = darkTheme ?? "blue-octopus";
1825
+ autoDarkTheme = darkTheme ?? "ink-octopus";
1810
1826
  autoLightTheme = lightTheme ?? "blue-octopus";
1811
1827
  const name = getDefaultTheme();
1812
1828
  previewThemeActive = false;
@@ -289,7 +289,7 @@ export interface InteractiveModeContext {
289
289
  handleHandoffCommand(customInstructions?: string): Promise<void>;
290
290
  handleContributionPrepCommand(customInstructions?: string): Promise<void>;
291
291
  handleMoveCommand(targetPath: string): Promise<void>;
292
- handleRenameCommand(title: string): Promise<void>;
292
+ handleRenameCommand(title?: string): Promise<void>;
293
293
  handleMemoryCommand(text: string): Promise<void>;
294
294
  handleSTTToggle(): Promise<void>;
295
295
  executeCompaction(
@@ -1,2 +1,2 @@
1
- Generate a 3-6 word title for a coding session from the user's first message. Capture the main task or topic.
1
+ Generate a 3-6 word title for a coding session from the supplied user message or conversation digest. If multiple messages are supplied, identify the primary/current task from the digest, weighting the newest substantive message highest; do not assume the first message is the task. If one message is supplied, capture its main task or topic.
2
2
  Output ONLY the title. No quotes or trailing punctuation.
@@ -2177,6 +2177,28 @@
2177
2177
  "testIds": "not_applicable"
2178
2178
  }
2179
2179
  },
2180
+ {
2181
+ "sourceId": "slash_command:star",
2182
+ "sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
2183
+ "sourceKind": "slash_command",
2184
+ "decision": "exclude",
2185
+ "rationale": "local session-discovery metadata command; no SDK operation counterpart or runtime/retention authority",
2186
+ "exclusionMetadata": {
2187
+ "adapterMappings": "not_applicable",
2188
+ "testIds": "not_applicable"
2189
+ }
2190
+ },
2191
+ {
2192
+ "sourceId": "slash_command:unstar",
2193
+ "sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
2194
+ "sourceKind": "slash_command",
2195
+ "decision": "exclude",
2196
+ "rationale": "local session-discovery metadata command; no SDK operation counterpart or runtime/retention authority",
2197
+ "exclusionMetadata": {
2198
+ "adapterMappings": "not_applicable",
2199
+ "testIds": "not_applicable"
2200
+ }
2201
+ },
2180
2202
  {
2181
2203
  "sourceId": "slash_command:resume",
2182
2204
  "sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
@@ -2275,6 +2297,24 @@
2275
2297
  "testIds": "not_applicable"
2276
2298
  }
2277
2299
  },
2300
+ {
2301
+ "sourceId": "slash_command:fork",
2302
+ "sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
2303
+ "sourceKind": "slash_command",
2304
+ "decision": "include",
2305
+ "sdkId": "session.branch",
2306
+ "adapterMappings": {
2307
+ "telegram": "generic_safe",
2308
+ "discord": "generic_safe",
2309
+ "slack": "generic_safe",
2310
+ "mcp": "generic_safe",
2311
+ "acp": "generic_safe",
2312
+ "daemonCli": "generic_safe"
2313
+ },
2314
+ "testIds": [
2315
+ "packages/coding-agent/test/sdk-operation-inventory.test.ts"
2316
+ ]
2317
+ },
2278
2318
  {
2279
2319
  "sourceId": "slash_command:rename",
2280
2320
  "sourceFile": "packages/coding-agent/src/slash-commands/builtin-registry.ts",
@@ -109,6 +109,7 @@ import {
109
109
  type FallbackAttemptToken,
110
110
  type FallbackTriggerClass,
111
111
  } from "@sayknow-cli/ai/utils/fallback-transport";
112
+ import { REPETITION_GUARD_ERROR_CODE } from "@sayknow-cli/ai/utils/stream-repetition-guard";
112
113
  import {
113
114
  BTW_MAX_ANSWER_UTF8_BYTES,
114
115
  BTW_MAX_QUESTION_UTF8_BYTES,
@@ -360,6 +361,7 @@ import { parseCommandArgs } from "../utils/command-args";
360
361
  import { type EditMode, resolveEditMode } from "../utils/edit-mode";
361
362
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
362
363
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
364
+ import { invalidateSessionTitleGeneration } from "../utils/session-title-generation";
363
365
  import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
364
366
  import { buildWorkflowIntentDiff, WORKFLOW_INTENT_DIFF_CUSTOM_TYPE } from "../workflow/workflow-intent-diff";
365
367
  import { buildWorkspaceTree, type WorkspaceTree } from "../workspace-tree";
@@ -7717,6 +7719,9 @@ export class AgentSession {
7717
7719
  }
7718
7720
 
7719
7721
  async prompt(text: string, options?: PromptOptions): Promise<void> {
7722
+ // A newer human prompt makes any in-flight /rename regeneration stale.
7723
+ if (options?.synthetic !== true && options?.attribution !== "agent")
7724
+ invalidateSessionTitleGeneration(this.sessionManager);
7720
7725
  this.#assertRecoveryHydrationPromoted();
7721
7726
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
7722
7727
 
@@ -8493,6 +8498,7 @@ export class AgentSession {
8493
8498
  * Queue a steering message to interrupt the agent mid-run.
8494
8499
  */
8495
8500
  async steer(text: string, images?: ImageContent[]): Promise<void> {
8501
+ invalidateSessionTitleGeneration(this.sessionManager);
8496
8502
  this.#assertRecoveryHydrationPromoted();
8497
8503
  if (text.startsWith("/")) {
8498
8504
  this.#throwIfExtensionCommand(text);
@@ -8511,6 +8517,7 @@ export class AgentSession {
8511
8517
  images?: ImageContent[],
8512
8518
  options?: Pick<PromptOptions, "followUpQueuePolicy">,
8513
8519
  ): Promise<void> {
8520
+ invalidateSessionTitleGeneration(this.sessionManager);
8514
8521
  this.#assertRecoveryHydrationPromoted();
8515
8522
  if (text.startsWith("/")) {
8516
8523
  this.#throwIfExtensionCommand(text);
@@ -13726,6 +13733,11 @@ export class AgentSession {
13726
13733
  #classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
13727
13734
  if (message.stopReason !== "error") return "none";
13728
13735
  if (message.errorKind === "provider_safety_stop") return "terminal";
13736
+ // A decode loop is deterministic for the submitted context: replaying the
13737
+ // identical conversation re-trips the guard and re-bills the full context.
13738
+ // Without this the message carries no transport facts and would fall
13739
+ // through to "unknown", which is admitted for bounded retry (#5627).
13740
+ if (message.errorCode === REPETITION_GUARD_ERROR_CODE) return "terminal";
13729
13741
  if (!message.errorMessage) return "none";
13730
13742
  const err = message.errorMessage;
13731
13743
  // Provider safety refusals (e.g. Anthropic stop_reason "refusal" /
@@ -1215,6 +1215,16 @@ function receiptMatches(
1215
1215
  tree?: unknown;
1216
1216
  };
1217
1217
  };
1218
+ // A receipt is keyed by its source and names exactly one destination. The
1219
+ // resume listing probes every (legacy source, v2 destination) pair, so reject
1220
+ // on these cheap receipt fields before hashing both transcripts and walking
1221
+ // the artifact manifest; the full conjunction below would be false anyway.
1222
+ if (
1223
+ record.destination?.path !== destination.path ||
1224
+ record.source?.path !== source.path ||
1225
+ record.source?.sha256 !== source.identity.sha256
1226
+ )
1227
+ return false;
1218
1228
  const exact = (
1219
1229
  recorded: { dev?: unknown; ino?: unknown; size?: unknown; mtimeNs?: unknown } | undefined,
1220
1230
  candidate: ManagedCandidate,
@@ -35,6 +35,7 @@ import type { ManagedLegacyLocalMigrationSource } from "../internal-urls/local-p
35
35
  import { assertSafePathComponent } from "../skc-runtime/session-layout";
36
36
  import { writeTextAtomic } from "../skc-runtime/state-writer";
37
37
  import * as git from "../utils/git";
38
+ import { invalidateSessionTitleGeneration } from "../utils/session-title-generation";
38
39
  import { ArtifactManager } from "./artifacts";
39
40
  import {
40
41
  type BlobPutResult,
@@ -96,7 +97,6 @@ import {
96
97
  memoryGuardCanonicalJson,
97
98
  memoryGuardSha256Hex,
98
99
  } from "./memory-guard-checkpoint-participant";
99
-
100
100
  import {
101
101
  type BashExecutionMessage,
102
102
  type CustomMessage,
@@ -110,6 +110,7 @@ import {
110
110
  stripInternalDetailsFields,
111
111
  } from "./messages";
112
112
  import { type SessionManagerReadAccess, sessionManagerReadCapability } from "./session-manager-internal";
113
+ import { isSessionIdStarred, setSessionIdStarred } from "./session-stars";
113
114
  import type {
114
115
  ManagedSessionSecurityContext,
115
116
  SessionStorage,
@@ -889,6 +890,8 @@ export interface SessionInfo {
889
890
  /** Working directory where the session was started. Empty string for old sessions. */
890
891
  cwd: string;
891
892
  title?: string;
893
+ /** Starred for discovery (see session-stars.ts); never affects resolution or retention. */
894
+ starred: boolean;
892
895
  /** Path to the parent session (if this session was forked). */
893
896
  parentSessionPath?: string;
894
897
  created: Date;
@@ -3819,6 +3822,11 @@ async function collectProjectSessions(cwd: string, storage: FileSessionStorage):
3819
3822
  return await collectSessionsFromFiles(listProjectSessionTranscriptFiles(cwd), storage);
3820
3823
  }
3821
3824
 
3825
+ /** Starred sessions first, each group keeping its incoming (recency) order. */
3826
+ export function prioritizeStarredSessions(sessions: readonly SessionInfo[]): SessionInfo[] {
3827
+ return [...sessions.filter(session => session.starred), ...sessions.filter(session => !session.starred)];
3828
+ }
3829
+
3822
3830
  function mergeSessionInventories(...inventories: SessionInfo[][]): SessionInfo[] {
3823
3831
  const sessions = new Map<string, SessionInfo>();
3824
3832
  for (const inventory of inventories) {
@@ -4229,6 +4237,7 @@ async function collectSessionFromFile(
4229
4237
  id: header.id,
4230
4238
  cwd: header.cwd ?? "",
4231
4239
  title: header.title ?? shortSummary,
4240
+ starred: isSessionIdStarred(header.id),
4232
4241
  parentSessionPath: header.parentSession,
4233
4242
  created: new Date(header.timestamp ?? ""),
4234
4243
  modified: stats.mtime,
@@ -6998,6 +7007,20 @@ export class SessionManager {
6998
7007
  return this.#sessionName;
6999
7008
  }
7000
7009
 
7010
+ isSessionStarred(): boolean {
7011
+ return isSessionIdStarred(this.getSessionId());
7012
+ }
7013
+
7014
+ /** Star or unstar this session. Returns `false` when it was already in that state. */
7015
+ async setSessionStarred(starred: boolean): Promise<boolean> {
7016
+ return setSessionIdStarred(this.getSessionId(), starred);
7017
+ }
7018
+
7019
+ /** Star or unstar a session listed in the resume picker (not necessarily this one). */
7020
+ async setSessionStarredForPicker(session: Pick<SessionInfo, "id">, starred: boolean): Promise<void> {
7021
+ await setSessionIdStarred(session.id, starred);
7022
+ }
7023
+
7001
7024
  /** Strip C0/C1 control characters (includes ESC, so removes ANSI sequences) and collapse whitespace. */
7002
7025
  static #sanitizeName(name: string): string {
7003
7026
  return name
@@ -7023,6 +7046,7 @@ export class SessionManager {
7023
7046
  const sanitized = SessionManager.#sanitizeName(name);
7024
7047
  if (!sanitized) return false;
7025
7048
 
7049
+ invalidateSessionTitleGeneration(this);
7026
7050
  this.#sessionName = sanitized;
7027
7051
  this.#titleSource = source;
7028
7052
  await this.#appendHeaderPatch({ title: sanitized, titleSource: source });
@@ -7219,6 +7243,7 @@ export class SessionManager {
7219
7243
  | PythonExecutionMessage
7220
7244
  | FileMentionMessage,
7221
7245
  ): string {
7246
+ if (message.role === "user") invalidateSessionTitleGeneration(this);
7222
7247
  const entry: SessionMessageEntry = {
7223
7248
  type: "message",
7224
7249
  id: generateId(this.#byId),
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Starred sessions: discovery-only metadata kept in one small index file under
3
+ * the agent directory, keyed by session id.
4
+ *
5
+ * Stars never touch transcripts. They sort sessions first in the resume picker
6
+ * and the sessions dashboard, and nothing else: `--continue`, ID-prefix
7
+ * resolution, deletion, and retention ignore them. A deleted session leaves a
8
+ * harmless stale id behind; a forked session gets a new id and starts unstarred.
9
+ */
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import { getAgentDir, logger } from "@sayknow-cli/utils";
13
+ import { withFileLock } from "../config/file-lock";
14
+
15
+ const INDEX_FILE_NAME = "session-stars.json";
16
+
17
+ interface StarIndexFile {
18
+ version: 1;
19
+ /** Session id → ISO time the star was added. */
20
+ starred: Record<string, string>;
21
+ }
22
+
23
+ let cache: { path: string; mtimeMs: number; size: number; ids: ReadonlySet<string> } | undefined;
24
+
25
+ export function getSessionStarIndexPath(agentDir: string = getAgentDir()): string {
26
+ return path.join(agentDir, INDEX_FILE_NAME);
27
+ }
28
+
29
+ function parseIndex(text: string): StarIndexFile {
30
+ const parsed = JSON.parse(text) as Partial<StarIndexFile>;
31
+ const starred: Record<string, string> = {};
32
+ if (parsed && typeof parsed === "object" && parsed.starred && typeof parsed.starred === "object") {
33
+ for (const [id, at] of Object.entries(parsed.starred)) {
34
+ if (typeof at === "string") starred[id] = at;
35
+ }
36
+ }
37
+ return { version: 1, starred };
38
+ }
39
+
40
+ /**
41
+ * Ids of starred sessions. Cheap to call per listed session: the parsed index is
42
+ * cached until the file's mtime or size changes. A missing or unreadable index
43
+ * means nothing is starred; listing never fails because of stars.
44
+ */
45
+ export function readStarredSessionIds(agentDir?: string): ReadonlySet<string> {
46
+ const indexPath = getSessionStarIndexPath(agentDir);
47
+ let stat: fs.Stats;
48
+ try {
49
+ stat = fs.statSync(indexPath);
50
+ } catch {
51
+ cache = undefined;
52
+ return new Set();
53
+ }
54
+ if (cache?.path === indexPath && cache.mtimeMs === stat.mtimeMs && cache.size === stat.size) return cache.ids;
55
+ try {
56
+ const ids = new Set(Object.keys(parseIndex(fs.readFileSync(indexPath, "utf8")).starred));
57
+ cache = { path: indexPath, mtimeMs: stat.mtimeMs, size: stat.size, ids };
58
+ return ids;
59
+ } catch (error) {
60
+ logger.warn("Ignoring unreadable session star index", { indexPath, error: String(error) });
61
+ return new Set();
62
+ }
63
+ }
64
+
65
+ export function isSessionIdStarred(sessionId: string, agentDir?: string): boolean {
66
+ return readStarredSessionIds(agentDir).has(sessionId);
67
+ }
68
+
69
+ /**
70
+ * Star or unstar a session by id. Returns `true` when the index changed and
71
+ * `false` when the session was already in the requested state. The
72
+ * read-modify-write runs under a cross-process lock and lands with an atomic
73
+ * rename, so two CLIs toggling different sessions cannot drop each other's star.
74
+ */
75
+ export async function setSessionIdStarred(sessionId: string, starred: boolean, agentDir?: string): Promise<boolean> {
76
+ if (!sessionId) throw new Error("A session id is required to change its star.");
77
+ const indexPath = getSessionStarIndexPath(agentDir);
78
+ await fs.promises.mkdir(path.dirname(indexPath), { recursive: true });
79
+ return withFileLock(indexPath, async () => {
80
+ let index: StarIndexFile;
81
+ try {
82
+ index = parseIndex(await fs.promises.readFile(indexPath, "utf8"));
83
+ } catch (error) {
84
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
85
+ // Refuse to overwrite an index we cannot read: that would silently drop every other star.
86
+ throw new Error(`Cannot update session stars: ${indexPath} is unreadable (${String(error)}).`);
87
+ }
88
+ index = { version: 1, starred: {} };
89
+ }
90
+ if (sessionId in index.starred === starred) return false;
91
+ if (starred) index.starred[sessionId] = new Date().toISOString();
92
+ else delete index.starred[sessionId];
93
+ const tmpPath = `${indexPath}.${process.pid}.${Date.now()}.tmp`;
94
+ try {
95
+ await fs.promises.writeFile(tmpPath, `${JSON.stringify(index, null, "\t")}\n`, "utf8");
96
+ await fs.promises.rename(tmpPath, indexPath);
97
+ } catch (error) {
98
+ await fs.promises.rm(tmpPath, { force: true }).catch(() => undefined);
99
+ throw error;
100
+ }
101
+ cache = undefined;
102
+ return true;
103
+ });
104
+ }