@sayknow-cli/coding-agent 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -1
- package/README.md +2 -2
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +7 -3
- package/dist/types/i18n/messages/en.d.ts +5 -14
- package/dist/types/modes/components/session-selector.d.ts +7 -3
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/components/user-message-selector.d.ts +5 -2
- package/dist/types/modes/components/user-message.d.ts +3 -1
- package/dist/types/modes/components/welcome.d.ts +34 -14
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +14 -2
- package/dist/types/modes/theme/defaults/index.d.ts +342 -0
- package/dist/types/modes/theme/theme.d.ts +5 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/session/session-manager.d.ts +9 -0
- package/dist/types/session/session-stars.d.ts +15 -0
- package/dist/types/tools/debug.d.ts +2 -2
- package/dist/types/tui/output-block.d.ts +12 -1
- package/dist/types/tui/utils.d.ts +1 -3
- package/dist/types/utils/session-title-generation.d.ts +6 -0
- package/dist/types/utils/title-generator.d.ts +11 -2
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +8 -0
- package/src/cli/session-picker.ts +4 -0
- package/src/config/keybindings.ts +8 -1
- package/src/config/settings-schema.ts +4 -3
- package/src/i18n/messages/de.messages.ts +4 -1
- package/src/i18n/messages/de.settings.ts +2 -0
- package/src/i18n/messages/de.ts +3 -12
- package/src/i18n/messages/en.ts +5 -14
- package/src/i18n/messages/es.messages.ts +4 -1
- package/src/i18n/messages/es.settings.ts +2 -0
- package/src/i18n/messages/es.ts +3 -12
- package/src/i18n/messages/fr.messages.ts +4 -1
- package/src/i18n/messages/fr.settings.ts +2 -0
- package/src/i18n/messages/fr.ts +3 -12
- package/src/i18n/messages/ja.messages.ts +4 -1
- package/src/i18n/messages/ja.settings.ts +2 -0
- package/src/i18n/messages/ja.ts +3 -12
- package/src/i18n/messages/ko.messages.ts +4 -1
- package/src/i18n/messages/ko.settings.ts +2 -0
- package/src/i18n/messages/ko.ts +4 -13
- package/src/i18n/messages/zh.messages.ts +2 -1
- package/src/i18n/messages/zh.settings.ts +2 -0
- package/src/i18n/messages/zh.ts +3 -12
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/lsp/render.ts +0 -1
- package/src/modes/action-registry.ts +1 -0
- package/src/modes/components/session-selector.ts +71 -21
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/presets.ts +5 -5
- package/src/modes/components/status-line/separators.ts +4 -0
- package/src/modes/components/tool-execution.ts +6 -19
- package/src/modes/components/tool-status-header.ts +103 -12
- package/src/modes/components/user-message-selector.ts +57 -15
- package/src/modes/components/user-message.ts +25 -15
- package/src/modes/components/welcome.ts +298 -576
- package/src/modes/controllers/command-controller.ts +56 -7
- package/src/modes/controllers/input-controller.ts +17 -0
- package/src/modes/controllers/selector-controller.ts +91 -10
- package/src/modes/interactive-mode.ts +85 -38
- package/src/modes/theme/defaults/glow-octopus.json +114 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/ink-octopus.json +114 -0
- package/src/modes/theme/defaults/violet-octopus.json +114 -0
- package/src/modes/theme/theme.ts +18 -2
- package/src/modes/types.ts +3 -1
- package/src/prompts/system/title-system.md +1 -1
- package/src/sdk/protocol/operation-inventory.generated.json +40 -0
- package/src/session/agent-session.ts +12 -0
- package/src/session/internal/managed-session-scope.ts +10 -0
- package/src/session/session-manager.ts +26 -1
- package/src/session/session-stars.ts +104 -0
- package/src/slash-commands/builtin-registry.ts +104 -4
- package/src/tools/debug.ts +0 -1
- package/src/tools/fetch.ts +0 -1
- package/src/tui/output-block.ts +29 -61
- package/src/tui/utils.ts +1 -8
- package/src/utils/session-title-generation.ts +29 -0
- package/src/utils/title-generator.ts +46 -2
|
@@ -51,6 +51,12 @@ import {
|
|
|
51
51
|
} from "../setup/provider-onboarding";
|
|
52
52
|
import { parseThinkingLevel } from "../thinking";
|
|
53
53
|
import { getDisplayChangelogEntries } from "../utils/changelog";
|
|
54
|
+
import {
|
|
55
|
+
beginSessionTitleGeneration,
|
|
56
|
+
invalidateSessionTitleGeneration,
|
|
57
|
+
isSessionTitleGenerationCurrent,
|
|
58
|
+
} from "../utils/session-title-generation";
|
|
59
|
+
import { buildConversationTitleInput, generateSessionTitle } from "../utils/title-generator";
|
|
54
60
|
import { buildContextReportText } from "./helpers/context-report";
|
|
55
61
|
import { buildFastStatusReport } from "./helpers/fast-status-report";
|
|
56
62
|
import { formatDuration } from "./helpers/format";
|
|
@@ -113,6 +119,45 @@ function fastStatusRoleTargets(): Array<{ id: SkcModelAssignmentTargetId; label:
|
|
|
113
119
|
}));
|
|
114
120
|
}
|
|
115
121
|
|
|
122
|
+
async function regenerateSessionTitle(runtime: SlashCommandRuntime): Promise<SlashCommandResult> {
|
|
123
|
+
try {
|
|
124
|
+
const generation = beginSessionTitleGeneration(runtime.sessionManager);
|
|
125
|
+
const input = buildConversationTitleInput(runtime.session.messages);
|
|
126
|
+
if (!input) {
|
|
127
|
+
return usage("Nothing to summarize yet — pass a title: /rename <title>", runtime);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const generated = await generateSessionTitle(
|
|
131
|
+
input,
|
|
132
|
+
runtime.session.modelRegistry,
|
|
133
|
+
runtime.settings,
|
|
134
|
+
runtime.session.sessionId,
|
|
135
|
+
runtime.session.model,
|
|
136
|
+
provider => runtime.session.agent.metadataForProvider(provider),
|
|
137
|
+
);
|
|
138
|
+
// A newer prompt, message, or explicit rename landed while the model ran: drop this result.
|
|
139
|
+
if (
|
|
140
|
+
!isSessionTitleGenerationCurrent(runtime.sessionManager, generation) ||
|
|
141
|
+
buildConversationTitleInput(runtime.session.messages) !== input
|
|
142
|
+
)
|
|
143
|
+
return commandConsumed();
|
|
144
|
+
if (!generated) {
|
|
145
|
+
return usage("Could not generate a session title — pass one: /rename <title>", runtime);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const stored = await runtime.sessionManager.setSessionName(generated, "user");
|
|
149
|
+
if (!stored) {
|
|
150
|
+
await runtime.output("Session name not changed (a user-set name takes precedence).");
|
|
151
|
+
return commandConsumed();
|
|
152
|
+
}
|
|
153
|
+
await runtime.notifyTitleChanged?.();
|
|
154
|
+
await runtime.output(`Session renamed to ${generated}.`);
|
|
155
|
+
return commandConsumed();
|
|
156
|
+
} catch (err) {
|
|
157
|
+
return usage(`Rename failed: ${errorMessage(err)}`, runtime);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
116
161
|
function toSlashCommandRuntime(runtime: TuiSlashCommandRuntime): SlashCommandRuntime {
|
|
117
162
|
const ctx = runtime.ctx;
|
|
118
163
|
return {
|
|
@@ -139,6 +184,29 @@ function toSlashCommandRuntime(runtime: TuiSlashCommandRuntime): SlashCommandRun
|
|
|
139
184
|
};
|
|
140
185
|
}
|
|
141
186
|
|
|
187
|
+
async function updateSessionStar(
|
|
188
|
+
starred: boolean,
|
|
189
|
+
command: ParsedSlashCommand,
|
|
190
|
+
runtime: SlashCommandRuntime,
|
|
191
|
+
): Promise<SlashCommandResult> {
|
|
192
|
+
if (command.args) return usage(`Usage: /${starred ? "star" : "unstar"}`, runtime);
|
|
193
|
+
if (!runtime.sessionManager.isPersisted()) {
|
|
194
|
+
await runtime.output("Session stars require a persisted session.");
|
|
195
|
+
return commandConsumed();
|
|
196
|
+
}
|
|
197
|
+
const changed = await runtime.sessionManager.setSessionStarred(starred);
|
|
198
|
+
await runtime.output(
|
|
199
|
+
changed
|
|
200
|
+
? starred
|
|
201
|
+
? "Session starred."
|
|
202
|
+
: "Session unstarred."
|
|
203
|
+
: starred
|
|
204
|
+
? "Session is already starred."
|
|
205
|
+
: "Session is not starred.",
|
|
206
|
+
);
|
|
207
|
+
return commandConsumed();
|
|
208
|
+
}
|
|
209
|
+
|
|
142
210
|
function parseProviderSetupSlashArgs(args: string): {
|
|
143
211
|
preset?: string;
|
|
144
212
|
compat?: string;
|
|
@@ -1619,6 +1687,24 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1619
1687
|
await runtime.ctx.handleContributionPrepCommand(command.args || undefined);
|
|
1620
1688
|
},
|
|
1621
1689
|
},
|
|
1690
|
+
{
|
|
1691
|
+
name: "star",
|
|
1692
|
+
priority: 91,
|
|
1693
|
+
description: "Star the current session for easier discovery",
|
|
1694
|
+
// Accept args at dispatch so the handler can consume them as a usage error
|
|
1695
|
+
// instead of silently forwarding `/star ...` to the model.
|
|
1696
|
+
allowArgs: true,
|
|
1697
|
+
handle: (command, runtime) => updateSessionStar(true, command, runtime),
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
name: "unstar",
|
|
1701
|
+
priority: 91,
|
|
1702
|
+
description: "Remove the star from the current session",
|
|
1703
|
+
// Accept args at dispatch so the handler can consume them as a usage error
|
|
1704
|
+
// instead of silently forwarding `/unstar ...` to the model.
|
|
1705
|
+
allowArgs: true,
|
|
1706
|
+
handle: (command, runtime) => updateSessionStar(false, command, runtime),
|
|
1707
|
+
},
|
|
1622
1708
|
{
|
|
1623
1709
|
name: "resume",
|
|
1624
1710
|
priority: 92,
|
|
@@ -1742,13 +1828,27 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1742
1828
|
await runtime.ctx.handleMemoryCommand(command.text);
|
|
1743
1829
|
},
|
|
1744
1830
|
},
|
|
1831
|
+
{
|
|
1832
|
+
name: "fork",
|
|
1833
|
+
description: "Choose an earlier prompt to continue in a new session",
|
|
1834
|
+
allowArgs: true,
|
|
1835
|
+
handleTui: async (command, runtime) => {
|
|
1836
|
+
runtime.ctx.editor.setText("");
|
|
1837
|
+
if (command.args.trim()) {
|
|
1838
|
+
runtime.ctx.showError("Usage: /fork");
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
runtime.ctx.showUserMessageSelector();
|
|
1842
|
+
},
|
|
1843
|
+
},
|
|
1745
1844
|
{
|
|
1746
1845
|
name: "rename",
|
|
1747
|
-
description: "Rename the current session",
|
|
1748
|
-
inlineHint: "
|
|
1846
|
+
description: "Rename the current session, or regenerate the title from the conversation",
|
|
1847
|
+
inlineHint: "[title]",
|
|
1749
1848
|
allowArgs: true,
|
|
1750
1849
|
handle: async (command, runtime) => {
|
|
1751
|
-
if (!command.args) return
|
|
1850
|
+
if (!command.args) return regenerateSessionTitle(runtime);
|
|
1851
|
+
invalidateSessionTitleGeneration(runtime.sessionManager);
|
|
1752
1852
|
const ok = await runtime.sessionManager.setSessionName(command.args, "user");
|
|
1753
1853
|
if (!ok) {
|
|
1754
1854
|
await runtime.output("Session name not changed (a user-set name takes precedence).");
|
|
@@ -1761,8 +1861,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1761
1861
|
handleTui: async (command, runtime) => {
|
|
1762
1862
|
const title = command.args.trim();
|
|
1763
1863
|
if (!title) {
|
|
1764
|
-
runtime.ctx.showError("Usage: /rename <title>");
|
|
1765
1864
|
runtime.ctx.editor.setText("");
|
|
1865
|
+
await runtime.ctx.handleRenameCommand();
|
|
1766
1866
|
return;
|
|
1767
1867
|
}
|
|
1768
1868
|
runtime.ctx.editor.setText("");
|
package/src/tools/debug.ts
CHANGED
package/src/tools/fetch.ts
CHANGED
package/src/tui/output-block.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Railed output container with optional header and sections.
|
|
3
3
|
*/
|
|
4
4
|
import {
|
|
5
5
|
ImageProtocol,
|
|
@@ -13,7 +13,7 @@ import type { Theme } from "../modes/theme/theme";
|
|
|
13
13
|
import { containsSixelSequence, getSixelLineMask } from "../utils/sixel";
|
|
14
14
|
import type { State } from "./types";
|
|
15
15
|
import type { RenderCache } from "./utils";
|
|
16
|
-
import {
|
|
16
|
+
import { Hasher, padToWidth, truncateToWidth } from "./utils";
|
|
17
17
|
|
|
18
18
|
export interface OutputBlockOptions {
|
|
19
19
|
header?: string;
|
|
@@ -21,17 +21,25 @@ export interface OutputBlockOptions {
|
|
|
21
21
|
state?: State;
|
|
22
22
|
sections?: Array<{ label?: string; lines: string[] }>;
|
|
23
23
|
width: number;
|
|
24
|
-
applyBg?: boolean;
|
|
25
24
|
}
|
|
26
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Render a tool output block as an open rail: a header line, then content hung
|
|
28
|
+
* off a single left rule in the state color. There is no enclosing box and no
|
|
29
|
+
* filled background in any state; the rail color alone carries running/success/error.
|
|
30
|
+
*
|
|
31
|
+
* ```
|
|
32
|
+
* ✓ Bash · 0.4s
|
|
33
|
+
* │ $ bun test
|
|
34
|
+
* ├ Output
|
|
35
|
+
* │ 12 pass
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
27
38
|
export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): string[] {
|
|
28
|
-
const { header, headerMeta, state, sections = [], width
|
|
29
|
-
const h = theme.boxSharp.horizontal;
|
|
30
|
-
const v = theme.boxSharp.vertical;
|
|
31
|
-
const cap = h.repeat(3);
|
|
39
|
+
const { header, headerMeta, state, sections = [], width } = options;
|
|
32
40
|
const lineWidth = Math.max(0, width);
|
|
33
|
-
//
|
|
34
|
-
const
|
|
41
|
+
// Rail colors: running/pending use accent, success recedes to dim, error/warning keep their colors.
|
|
42
|
+
const railColor: "error" | "warning" | "accent" | "dim" =
|
|
35
43
|
state === "error"
|
|
36
44
|
? "error"
|
|
37
45
|
: state === "warning"
|
|
@@ -39,53 +47,19 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
39
47
|
: state === "running" || state === "pending"
|
|
40
48
|
? "accent"
|
|
41
49
|
: "dim";
|
|
42
|
-
const
|
|
43
|
-
const bgFn = (() => {
|
|
44
|
-
if (!state || !applyBg) return undefined;
|
|
45
|
-
const bgAnsi = theme.getBgAnsi(getStateBgColor(state));
|
|
46
|
-
// Keep block background stable even if inner content contains SGR resets (e.g. "\x1b[0m"),
|
|
47
|
-
// which would otherwise clear the outer background mid-line.
|
|
48
|
-
return (text: string) => {
|
|
49
|
-
const stabilized = text
|
|
50
|
-
.replace(/\x1b\[(?:0)?m/g, m => `${m}${bgAnsi}`)
|
|
51
|
-
.replace(/\x1b\[49m/g, m => `${m}${bgAnsi}`);
|
|
52
|
-
return `${bgAnsi}${stabilized}\x1b[49m`;
|
|
53
|
-
};
|
|
54
|
-
})();
|
|
55
|
-
|
|
56
|
-
const buildBarLine = (leftChar: string, rightChar: string, label?: string, meta?: string): string => {
|
|
57
|
-
const left = border(`${leftChar}${cap}`);
|
|
58
|
-
const right = border(rightChar);
|
|
59
|
-
if (lineWidth <= 0) return left + right;
|
|
60
|
-
const labelText = [label, meta].filter(Boolean).join(theme.sep.dot);
|
|
61
|
-
const rawLabel = labelText ? ` ${labelText} ` : " ";
|
|
62
|
-
const leftWidth = visibleWidth(left);
|
|
63
|
-
const rightWidth = visibleWidth(right);
|
|
64
|
-
const maxLabelWidth = Math.max(0, lineWidth - leftWidth - rightWidth);
|
|
65
|
-
const trimmedLabel = truncateToWidth(rawLabel, maxLabelWidth);
|
|
66
|
-
const labelWidth = visibleWidth(trimmedLabel);
|
|
67
|
-
const fillCount = Math.max(0, lineWidth - leftWidth - labelWidth - rightWidth);
|
|
68
|
-
return `${left}${trimmedLabel}${border(h.repeat(fillCount))}${right}`;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const contentPrefix = border(`${v} `);
|
|
72
|
-
const contentSuffix = border(v);
|
|
73
|
-
const contentWidth = Math.max(0, lineWidth - visibleWidth(contentPrefix) - visibleWidth(contentSuffix));
|
|
50
|
+
const rail = (text: string) => theme.fg(railColor, text);
|
|
74
51
|
const lines: string[] = [];
|
|
52
|
+
const labelText = [header, headerMeta].filter(Boolean).join(theme.sep.dot);
|
|
53
|
+
if (labelText) lines.push(padToWidth(truncateToWidth(labelText, lineWidth), lineWidth));
|
|
75
54
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
const hasSections = sections.length > 0;
|
|
81
|
-
const normalizedSections = hasSections ? sections : [{ lines: [] }];
|
|
55
|
+
const contentPrefix = rail(`${theme.boxSharp.vertical} `);
|
|
56
|
+
const contentWidth = Math.max(0, lineWidth - visibleWidth(contentPrefix));
|
|
82
57
|
|
|
83
|
-
for (
|
|
84
|
-
const section = normalizedSections[i];
|
|
58
|
+
for (const section of sections) {
|
|
85
59
|
if (section.label) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
60
|
+
const tee = rail(`${theme.boxSharp.teeRight} `);
|
|
61
|
+
const label = truncateToWidth(theme.fg("dim", section.label), contentWidth);
|
|
62
|
+
lines.push(padToWidth(`${tee}${label}`, lineWidth));
|
|
89
63
|
}
|
|
90
64
|
const allLines = section.lines.flatMap(l => l.split("\n"));
|
|
91
65
|
const fallbackActive = isTerminalGraphicsFallbackActive();
|
|
@@ -102,18 +76,13 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
102
76
|
const wrappedLines = wrapTextWithAnsi(line.trimEnd(), contentWidth);
|
|
103
77
|
for (const wrappedLine of wrappedLines) {
|
|
104
78
|
const innerPadding = padding(Math.max(0, contentWidth - visibleWidth(wrappedLine)));
|
|
105
|
-
|
|
106
|
-
lines.push(padToWidth(fullLine, lineWidth, bgFn));
|
|
79
|
+
lines.push(padToWidth(`${contentPrefix}${wrappedLine}${innerPadding}`, lineWidth));
|
|
107
80
|
}
|
|
108
81
|
}
|
|
109
82
|
}
|
|
110
83
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const bottomFillCount = Math.max(0, lineWidth - visibleWidth(bottomLeft) - visibleWidth(bottomRight));
|
|
114
|
-
const bottomLine = `${bottomLeft}${border(h.repeat(bottomFillCount))}${bottomRight}`;
|
|
115
|
-
lines.push(padToWidth(bottomLine, lineWidth, bgFn));
|
|
116
|
-
|
|
84
|
+
// A block with neither header nor content still marks its place with a bare rail row.
|
|
85
|
+
if (lines.length === 0) lines.push(padToWidth(rail(theme.boxSharp.vertical), lineWidth));
|
|
117
86
|
return lines;
|
|
118
87
|
}
|
|
119
88
|
|
|
@@ -147,7 +116,6 @@ export class CachedOutputBlock {
|
|
|
147
116
|
h.optional(options.header);
|
|
148
117
|
h.optional(options.headerMeta);
|
|
149
118
|
h.optional(options.state);
|
|
150
|
-
h.bool(options.applyBg ?? true);
|
|
151
119
|
h.bool(isTerminalGraphicsFallbackActive());
|
|
152
120
|
if (options.sections) {
|
|
153
121
|
for (const s of options.sections) {
|
package/src/tui/utils.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Shared helpers for tool-rendered UI components.
|
|
3
3
|
*/
|
|
4
4
|
import { padding, visibleWidth } from "@sayknow-cli/tui";
|
|
5
|
-
import type { Theme
|
|
6
|
-
import type { State } from "./types";
|
|
5
|
+
import type { Theme } from "../modes/theme/theme";
|
|
7
6
|
|
|
8
7
|
export { Ellipsis, truncateToWidth } from "@sayknow-cli/tui";
|
|
9
8
|
|
|
@@ -95,9 +94,3 @@ export function padToWidth(text: string, width: number, bgFn?: (s: string) => st
|
|
|
95
94
|
const padded = paddingNeeded > 0 ? text + padding(paddingNeeded) : text;
|
|
96
95
|
return bgFn ? bgFn(padded) : padded;
|
|
97
96
|
}
|
|
98
|
-
|
|
99
|
-
export function getStateBgColor(state: State): ThemeBg {
|
|
100
|
-
if (state === "success") return "toolSuccessBg";
|
|
101
|
-
if (state === "error") return "toolErrorBg";
|
|
102
|
-
return "toolPendingBg";
|
|
103
|
-
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory generation lease for asynchronous session-title regeneration.
|
|
3
|
+
*
|
|
4
|
+
* The lease is keyed by the session manager instance so TUI and ACP command
|
|
5
|
+
* dispatchers share the same ordering state without adding protocol fields to
|
|
6
|
+
* the persisted session format.
|
|
7
|
+
*/
|
|
8
|
+
const generations = new WeakMap<object, number>();
|
|
9
|
+
|
|
10
|
+
function currentGeneration(owner: object): number {
|
|
11
|
+
return generations.get(owner) ?? 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Start a new regeneration and invalidate every older completion. */
|
|
15
|
+
export function beginSessionTitleGeneration(owner: object): number {
|
|
16
|
+
const generation = currentGeneration(owner) + 1;
|
|
17
|
+
generations.set(owner, generation);
|
|
18
|
+
return generation;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Invalidate pending regeneration after a newer session mutation is accepted. */
|
|
22
|
+
export function invalidateSessionTitleGeneration(owner: object): void {
|
|
23
|
+
generations.set(owner, currentGeneration(owner) + 1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Return whether a regeneration still owns the latest generation lease. */
|
|
27
|
+
export function isSessionTitleGenerationCurrent(owner: object, generation: number): boolean {
|
|
28
|
+
return currentGeneration(owner) === generation;
|
|
29
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
|
|
6
|
+
import type { AgentMessage } from "@sayknow-cli/agent-core";
|
|
6
7
|
import { type Api, type AssistantMessage, completeSimple, type Model, type Tool } from "@sayknow-cli/ai";
|
|
7
8
|
import { logger, prompt } from "@sayknow-cli/utils";
|
|
8
9
|
import type { ModelRegistry } from "../config/model-registry";
|
|
@@ -16,6 +17,9 @@ const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
|
|
|
16
17
|
const DEFAULT_TERMINAL_TITLE = "SKC";
|
|
17
18
|
const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
18
19
|
|
|
20
|
+
const MAX_TITLE_CONVERSATION_MESSAGES = 6;
|
|
21
|
+
const MAX_TITLE_MESSAGE_CHARS = 400;
|
|
22
|
+
|
|
19
23
|
const MAX_INPUT_CHARS = 2000;
|
|
20
24
|
const TITLE_MAX_TOKENS = 30;
|
|
21
25
|
const REASONING_SAFE_MAX_TOKENS = 1024;
|
|
@@ -28,6 +32,44 @@ const SET_TITLE_TOOL_NAME = "set_title";
|
|
|
28
32
|
const MAX_TITLE_CHARS = 80;
|
|
29
33
|
const MAX_TITLE_WORDS = 12;
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Build a bounded conversation digest for title regeneration. Regeneration
|
|
37
|
+
* reads several messages so a throwaway first prompt can be corrected, while
|
|
38
|
+
* the automatic first-message path intentionally uses only its initial input.
|
|
39
|
+
*/
|
|
40
|
+
export function buildConversationTitleInput(messages: readonly AgentMessage[]): string | undefined {
|
|
41
|
+
const userMessages: string[] = [];
|
|
42
|
+
for (const message of messages) {
|
|
43
|
+
if (message.role !== "user") continue;
|
|
44
|
+
const content = message.content;
|
|
45
|
+
const text =
|
|
46
|
+
typeof content === "string"
|
|
47
|
+
? content
|
|
48
|
+
: content
|
|
49
|
+
.filter(block => block.type === "text")
|
|
50
|
+
.map(block => block.text)
|
|
51
|
+
.join("\n");
|
|
52
|
+
const trimmed = text.trim();
|
|
53
|
+
if (trimmed) userMessages.push(trimmed);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (userMessages.length === 0) return undefined;
|
|
57
|
+
|
|
58
|
+
const keptMessages =
|
|
59
|
+
userMessages.length > MAX_TITLE_CONVERSATION_MESSAGES
|
|
60
|
+
? [userMessages[0]!, ...userMessages.slice(-(MAX_TITLE_CONVERSATION_MESSAGES - 1))]
|
|
61
|
+
: userMessages;
|
|
62
|
+
// Reserve separator and ellipsis room so the digest is already within the
|
|
63
|
+
// generator's MAX_INPUT_CHARS bound; its existing truncation is a no-op here.
|
|
64
|
+
const maxMessageChars = Math.min(
|
|
65
|
+
MAX_TITLE_MESSAGE_CHARS,
|
|
66
|
+
Math.floor((MAX_INPUT_CHARS - (keptMessages.length - 1) - keptMessages.length) / keptMessages.length),
|
|
67
|
+
);
|
|
68
|
+
return keptMessages
|
|
69
|
+
.map(message => (message.length > maxMessageChars ? `${message.slice(0, maxMessageChars)}…` : message))
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
31
73
|
const setTitleTool: Tool = {
|
|
32
74
|
name: SET_TITLE_TOOL_NAME,
|
|
33
75
|
description: "Set the generated session title.",
|
|
@@ -57,9 +99,11 @@ function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel
|
|
|
57
99
|
}
|
|
58
100
|
|
|
59
101
|
/**
|
|
60
|
-
* Generate a title for a session based on the
|
|
102
|
+
* Generate a title for a session based on the provided user-message input.
|
|
103
|
+
* Automatic titles pass the first user message; regeneration passes a bounded
|
|
104
|
+
* conversation digest.
|
|
61
105
|
*
|
|
62
|
-
* @param firstMessage The first user message
|
|
106
|
+
* @param firstMessage The first user message or bounded conversation digest
|
|
63
107
|
* @param registry Model registry
|
|
64
108
|
* @param settings Settings used to resolve the smol role
|
|
65
109
|
* @param sessionId Optional session id for sticky API key selection
|