@sayknow-cli/coding-agent 0.3.11 → 0.3.12
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 +0 -21
- package/dist/types/build-metadata.d.ts +7 -0
- package/dist/types/capability/mcp.d.ts +3 -1
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +6 -0
- package/dist/types/modes/components/status-line.d.ts +1 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -0
- package/dist/types/notifications/telegram-daemon.d.ts +2 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +4 -3
- package/dist/types/runtime-mcp/types.d.ts +4 -3
- package/dist/types/session/agent-session.d.ts +16 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/tools/skill.d.ts +9 -8
- package/dist/types/utils/linkedom.d.ts +4 -0
- package/dist/types/utils/pasted-image-path.d.ts +5 -3
- package/package.json +7 -7
- package/scripts/build-binary.ts +2 -43
- package/scripts/compile-args.ts +106 -0
- package/scripts/generate-docs-index.ts +1 -0
- package/src/build-metadata.ts +104 -0
- package/src/capability/mcp.ts +3 -1
- package/src/cli/mcp-cli.ts +108 -26
- package/src/cli.ts +21 -0
- package/src/commands/mcp.ts +8 -8
- package/src/commands/stats.ts +6 -2
- package/src/config/keybindings.ts +2 -2
- package/src/config/mcp-schema.json +21 -17
- package/src/config/model-profiles.ts +23 -23
- package/src/discovery/builtin.ts +14 -0
- package/src/discovery/mcp-json.ts +14 -0
- package/src/eval/js/executor.ts +7 -1
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/skc-protocol.ts +14 -1
- package/src/modes/acp/acp-agent.ts +72 -16
- package/src/modes/acp/acp-client-bridge.ts +27 -5
- package/src/modes/acp/acp-event-mapper.ts +39 -1
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/hook-selector.ts +4 -0
- package/src/modes/components/model-selector.ts +17 -6
- package/src/modes/components/session-selector.ts +2 -2
- package/src/modes/components/status-line/presets.ts +5 -19
- package/src/modes/components/status-line/segments.ts +14 -0
- package/src/modes/components/status-line/types.ts +6 -0
- package/src/modes/components/status-line.ts +13 -6
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/welcome.ts +4 -1
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +9 -9
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +7 -3
- package/src/modes/interactive-mode.ts +12 -4
- package/src/modes/rpc/rpc-client.ts +30 -6
- package/src/modes/utils/ui-helpers.ts +154 -32
- package/src/notifications/telegram-daemon.ts +28 -2
- package/src/prompts/tools/skill.md +2 -2
- package/src/runtime-mcp/config-writer.ts +4 -3
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/types.ts +4 -3
- package/src/session/agent-session.ts +117 -3
- package/src/session/session-manager.ts +68 -40
- package/src/skc-runtime/state-runtime.ts +105 -12
- package/src/skc-runtime/workflow-manifest.generated.json +4 -28
- package/src/skc-runtime/workflow-manifest.ts +1 -7
- package/src/slash-commands/builtin-registry.ts +9 -4
- package/src/tools/bash-interactive.ts +9 -4
- package/src/tools/browser/readable.ts +2 -2
- package/src/tools/fetch.ts +5 -5
- package/src/tools/skill.ts +14 -11
- package/src/utils/linkedom.ts +14 -0
- package/src/utils/pasted-image-path.ts +23 -9
- package/src/web/scrapers/arxiv.ts +2 -2
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/iacr.ts +2 -2
- package/src/web/scrapers/readthedocs.ts +2 -2
- package/src/web/scrapers/twitter.ts +3 -2
- package/src/web/scrapers/wikipedia.ts +2 -2
|
@@ -22,18 +22,38 @@ import type {
|
|
|
22
22
|
ClientBridgeTerminalHandle,
|
|
23
23
|
} from "../../session/client-bridge";
|
|
24
24
|
|
|
25
|
+
type AcpPermissionMode = "auto" | "prompt" | "always-allow";
|
|
26
|
+
|
|
27
|
+
const ACP_PERMISSION_MODE_ENV = "SKC_ACP_PERMISSION_MODE";
|
|
28
|
+
|
|
29
|
+
function parseAcpPermissionMode(value: unknown): AcpPermissionMode {
|
|
30
|
+
if (value === "auto" || value === "prompt" || value === "always-allow") return value;
|
|
31
|
+
return "prompt";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Client metadata is authoritative; the process environment is only a fallback when that field is absent. */
|
|
35
|
+
function resolveAcpPermissionMode(clientCapabilities: ClientCapabilities | undefined): AcpPermissionMode {
|
|
36
|
+
const meta = clientCapabilities?._meta;
|
|
37
|
+
if (typeof meta === "object" && meta !== null) {
|
|
38
|
+
const skc = (meta as { skc?: unknown }).skc;
|
|
39
|
+
if (typeof skc === "object" && skc !== null && "permissionHandling" in skc) {
|
|
40
|
+
return parseAcpPermissionMode((skc as { permissionHandling?: unknown }).permissionHandling);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return parseAcpPermissionMode(process.env[ACP_PERMISSION_MODE_ENV]);
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
export function createAcpClientBridge(
|
|
26
47
|
connection: AgentSideConnection,
|
|
27
48
|
sessionId: string,
|
|
28
49
|
clientCapabilities: ClientCapabilities | undefined,
|
|
29
50
|
): ClientBridge {
|
|
51
|
+
const promptPermission = resolveAcpPermissionMode(clientCapabilities) === "prompt";
|
|
30
52
|
const capabilities: ClientBridgeCapabilities = {
|
|
31
53
|
readTextFile: clientCapabilities?.fs?.readTextFile === true,
|
|
32
54
|
writeTextFile: clientCapabilities?.fs?.writeTextFile === true,
|
|
33
55
|
terminal: clientCapabilities?.terminal === true,
|
|
34
|
-
|
|
35
|
-
// the agent's policy choice rather than a client capability.
|
|
36
|
-
requestPermission: true,
|
|
56
|
+
requestPermission: promptPermission,
|
|
37
57
|
};
|
|
38
58
|
|
|
39
59
|
const bridge: ClientBridge = { capabilities, deferAgentInitiatedTurns: true };
|
|
@@ -65,8 +85,10 @@ export function createAcpClientBridge(
|
|
|
65
85
|
createTerminalHandle(connection, sessionId, params);
|
|
66
86
|
}
|
|
67
87
|
|
|
68
|
-
|
|
69
|
-
requestPermission
|
|
88
|
+
if (promptPermission) {
|
|
89
|
+
bridge.requestPermission = (toolCall, options, signal) =>
|
|
90
|
+
requestPermission(connection, sessionId, toolCall, options, signal);
|
|
91
|
+
}
|
|
70
92
|
|
|
71
93
|
return bridge;
|
|
72
94
|
}
|
|
@@ -27,6 +27,8 @@ interface AcpEventMapperOptions {
|
|
|
27
27
|
* before emitting `ToolCallLocation` entries.
|
|
28
28
|
*/
|
|
29
29
|
cwd?: string;
|
|
30
|
+
/** Phase to expose after compaction ends. Prompt-bound compaction resumes responding; idle maintenance returns idle. */
|
|
31
|
+
compactionEndPhase?: "responding" | "idle";
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
interface ContentArrayContainer {
|
|
@@ -236,8 +238,44 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
236
238
|
case "turn_start":
|
|
237
239
|
case "turn_end":
|
|
238
240
|
case "message_start":
|
|
241
|
+
return [];
|
|
239
242
|
case "auto_compaction_start":
|
|
240
|
-
|
|
243
|
+
return [
|
|
244
|
+
toSessionNotification(sessionId, {
|
|
245
|
+
sessionUpdate: "session_info_update",
|
|
246
|
+
_meta: {
|
|
247
|
+
skcPhase: "compacting",
|
|
248
|
+
skcCompactionState: "start",
|
|
249
|
+
skcCompactionTrigger: event.reason,
|
|
250
|
+
skcCompactionAction: event.action,
|
|
251
|
+
running: true,
|
|
252
|
+
skcRunning: true,
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
];
|
|
256
|
+
case "auto_compaction_end": {
|
|
257
|
+
const phase = options.compactionEndPhase ?? "responding";
|
|
258
|
+
const running = phase !== "idle";
|
|
259
|
+
const meta: Record<string, unknown> = {
|
|
260
|
+
skcPhase: phase,
|
|
261
|
+
skcCompactionState: "end",
|
|
262
|
+
skcCompactionAction: event.action,
|
|
263
|
+
skcCompactionAborted: event.aborted,
|
|
264
|
+
skcCompactionWillRetry: event.willRetry,
|
|
265
|
+
running,
|
|
266
|
+
skcRunning: running,
|
|
267
|
+
};
|
|
268
|
+
if (event.skipped !== undefined) {
|
|
269
|
+
meta.skcCompactionSkipped = event.skipped;
|
|
270
|
+
}
|
|
271
|
+
if (event.errorMessage !== undefined) {
|
|
272
|
+
meta.skcCompactionErrorMessage = event.errorMessage;
|
|
273
|
+
}
|
|
274
|
+
if (event.continuationSkipReason !== undefined) {
|
|
275
|
+
meta.skcCompactionContinuationSkipReason = event.continuationSkipReason;
|
|
276
|
+
}
|
|
277
|
+
return [toSessionNotification(sessionId, { sessionUpdate: "session_info_update", _meta: meta })];
|
|
278
|
+
}
|
|
241
279
|
case "auto_retry_start":
|
|
242
280
|
case "auto_retry_end":
|
|
243
281
|
case "retry_fallback_applied":
|
|
@@ -269,7 +269,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
269
269
|
displayImage.data,
|
|
270
270
|
displayImage.mimeType,
|
|
271
271
|
{ fallbackColor: (text: string) => theme.fg("toolOutput", text) },
|
|
272
|
-
resolveImageOptions(),
|
|
272
|
+
{ ...resolveImageOptions(), refetch: () => displayImage.data },
|
|
273
273
|
),
|
|
274
274
|
),
|
|
275
275
|
});
|
|
@@ -435,6 +435,10 @@ export class HookSelectorComponent extends Container {
|
|
|
435
435
|
this.#updateList();
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
hasActiveInlineInput(): boolean {
|
|
439
|
+
return this.#inlineEditor !== undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
438
442
|
#updateList(): void {
|
|
439
443
|
if (this.#wrapFocused && this.#focusAwareList) {
|
|
440
444
|
this.#focusAwareList.setState(this.#options, this.#selectedIndex, this.#maxVisible);
|
|
@@ -119,6 +119,7 @@ export type ModelSelectorSelection =
|
|
|
119
119
|
interface PendingThinkingChoice {
|
|
120
120
|
item: ModelItem | CanonicalModelItem;
|
|
121
121
|
role: SkcModelAssignmentTargetId | null;
|
|
122
|
+
roles?: readonly SkcModelAssignmentTargetId[];
|
|
122
123
|
levels: ThinkingLevel[];
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -1387,8 +1388,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
1387
1388
|
}
|
|
1388
1389
|
|
|
1389
1390
|
#renderThinkingMenu(choice: PendingThinkingChoice): void {
|
|
1390
|
-
const targetLabel =
|
|
1391
|
-
choice.
|
|
1391
|
+
const targetLabel = choice.roles
|
|
1392
|
+
? choice.roles.includes("default")
|
|
1393
|
+
? "all targets"
|
|
1394
|
+
: "all role agents"
|
|
1395
|
+
: choice.role === null
|
|
1396
|
+
? t("modelSelector.temporaryModel")
|
|
1397
|
+
: SKC_MODEL_ASSIGNMENT_TARGETS[choice.role].name;
|
|
1392
1398
|
this.#listContainer.addChild(new Spacer(1));
|
|
1393
1399
|
this.#listContainer.addChild(
|
|
1394
1400
|
new Text(
|
|
@@ -1699,14 +1705,16 @@ export class ModelSelectorComponent extends Container {
|
|
|
1699
1705
|
const level = choice.levels[this.#selectedThinkingIndex];
|
|
1700
1706
|
if (!level) return;
|
|
1701
1707
|
this.#pendingThinkingChoice = undefined;
|
|
1702
|
-
this.#handleSelect(choice.item, choice.role, level);
|
|
1708
|
+
this.#handleSelect(choice.item, choice.role, level, choice.roles);
|
|
1703
1709
|
return;
|
|
1704
1710
|
}
|
|
1705
1711
|
if (getKeybindings().matches(keyData, "tui.select.cancel")) {
|
|
1706
1712
|
this.#pendingThinkingChoice = undefined;
|
|
1707
1713
|
if (choice.role !== null) {
|
|
1708
1714
|
this.#pendingActionItem = choice.item;
|
|
1709
|
-
this.#selectedActionIndex =
|
|
1715
|
+
this.#selectedActionIndex = choice.roles
|
|
1716
|
+
? SKC_MODEL_ASSIGNMENT_TARGET_IDS.length + (choice.roles.includes("default") ? 1 : 0)
|
|
1717
|
+
: Math.max(0, SKC_MODEL_ASSIGNMENT_TARGET_IDS.indexOf(choice.role));
|
|
1710
1718
|
}
|
|
1711
1719
|
this.#updateList();
|
|
1712
1720
|
}
|
|
@@ -1735,9 +1743,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
1735
1743
|
): void {
|
|
1736
1744
|
const itemThinkingLevel = thinkingLevel ?? item.thinkingLevel;
|
|
1737
1745
|
const hasExplicitThinkingChoice = thinkingLevel !== undefined || item.explicitThinkingLevel === true;
|
|
1738
|
-
|
|
1746
|
+
const needsExplicitThinkingChoice = roles
|
|
1747
|
+
? roles.some(targetRole => requiresExplicitThinkingChoice(item.model, targetRole))
|
|
1748
|
+
: requiresExplicitThinkingChoice(item.model, role);
|
|
1749
|
+
if (!hasExplicitThinkingChoice && needsExplicitThinkingChoice) {
|
|
1739
1750
|
const levels = getSelectableThinkingLevels(item.model);
|
|
1740
|
-
this.#pendingThinkingChoice = { item, role, levels };
|
|
1751
|
+
this.#pendingThinkingChoice = { item, role, roles, levels };
|
|
1741
1752
|
this.#selectedThinkingIndex = this.#getInitialThinkingChoiceIndex(item, levels);
|
|
1742
1753
|
this.#updateList();
|
|
1743
1754
|
return;
|
|
@@ -180,7 +180,7 @@ class SessionList implements Component {
|
|
|
180
180
|
|
|
181
181
|
// Add keybinding hint
|
|
182
182
|
lines.push("");
|
|
183
|
-
lines.push(theme.fg("muted", " [Del to delete, Enter to select, Esc to cancel]"));
|
|
183
|
+
lines.push(theme.fg("muted", " [Del to delete selected transcript/artifacts, Enter to select, Esc to cancel]"));
|
|
184
184
|
|
|
185
185
|
return lines;
|
|
186
186
|
}
|
|
@@ -307,7 +307,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
307
307
|
#showDeleteConfirmation(session: SessionInfo): void {
|
|
308
308
|
const displayName = session.title || session.firstMessage.slice(0, 40) || session.id;
|
|
309
309
|
this.#confirmationDialog = new HookSelectorComponent(
|
|
310
|
-
`Delete session?\n${displayName}
|
|
310
|
+
`Delete selected session transcript and artifacts?\n${displayName}\nThis cannot be undone. Other sessions and topic/history metadata are not deleted.`,
|
|
311
311
|
["Yes", "No"],
|
|
312
312
|
async (option: string) => {
|
|
313
313
|
if (option === "Yes" && this.#onDelete) {
|
|
@@ -3,19 +3,7 @@ import type { PresetDef, StatusLinePreset } from "./types";
|
|
|
3
3
|
export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
4
4
|
default: {
|
|
5
5
|
leftSegments: ["model", "mode", "git", "pr", "path"],
|
|
6
|
-
rightSegments: [
|
|
7
|
-
"session_name",
|
|
8
|
-
"jobs",
|
|
9
|
-
"token_in",
|
|
10
|
-
"token_out",
|
|
11
|
-
"cache_read",
|
|
12
|
-
"token_rate",
|
|
13
|
-
"context_bar",
|
|
14
|
-
"context_pct",
|
|
15
|
-
"cost",
|
|
16
|
-
"usage",
|
|
17
|
-
"time_spent",
|
|
18
|
-
],
|
|
6
|
+
rightSegments: ["session_name", "jobs", "token_rate", "cost"],
|
|
19
7
|
separator: "slash",
|
|
20
8
|
segmentOptions: {
|
|
21
9
|
model: { showThinkingLevel: true },
|
|
@@ -26,7 +14,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
26
14
|
|
|
27
15
|
"default-usage": {
|
|
28
16
|
leftSegments: ["model", "mode", "git", "pr", "path"],
|
|
29
|
-
rightSegments: ["session_name", "jobs", "token_rate", "usage", "
|
|
17
|
+
rightSegments: ["session_name", "jobs", "token_rate", "usage", "cost"],
|
|
30
18
|
separator: "slash",
|
|
31
19
|
segmentOptions: {
|
|
32
20
|
model: { showThinkingLevel: true },
|
|
@@ -47,7 +35,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
47
35
|
|
|
48
36
|
compact: {
|
|
49
37
|
leftSegments: ["model", "mode", "git", "pr"],
|
|
50
|
-
rightSegments: ["session_name", "jobs", "cost"
|
|
38
|
+
rightSegments: ["session_name", "jobs", "cost"],
|
|
51
39
|
separator: "slash",
|
|
52
40
|
segmentOptions: {
|
|
53
41
|
model: { showThinkingLevel: false },
|
|
@@ -65,7 +53,6 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
65
53
|
"token_rate",
|
|
66
54
|
"cache_read",
|
|
67
55
|
"cost",
|
|
68
|
-
"context_pct",
|
|
69
56
|
"time_spent",
|
|
70
57
|
"time",
|
|
71
58
|
],
|
|
@@ -90,7 +77,6 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
90
77
|
"cache_write",
|
|
91
78
|
"token_rate",
|
|
92
79
|
"cost",
|
|
93
|
-
"context_pct",
|
|
94
80
|
"context_total",
|
|
95
81
|
"time_spent",
|
|
96
82
|
"time",
|
|
@@ -107,7 +93,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
107
93
|
ascii: {
|
|
108
94
|
// No Nerd Font dependencies
|
|
109
95
|
leftSegments: ["model", "mode", "path", "git", "pr"],
|
|
110
|
-
rightSegments: ["session_name", "jobs", "token_total", "cost"
|
|
96
|
+
rightSegments: ["session_name", "jobs", "token_total", "cost"],
|
|
111
97
|
separator: "ascii",
|
|
112
98
|
segmentOptions: {
|
|
113
99
|
model: { showThinkingLevel: true },
|
|
@@ -119,7 +105,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
119
105
|
custom: {
|
|
120
106
|
// User-defined - these are just defaults that get overridden
|
|
121
107
|
leftSegments: ["model", "mode", "path", "git", "pr"],
|
|
122
|
-
rightSegments: ["session_name", "jobs", "token_total", "cost"
|
|
108
|
+
rightSegments: ["session_name", "jobs", "token_total", "cost"],
|
|
123
109
|
separator: "slash",
|
|
124
110
|
segmentOptions: {},
|
|
125
111
|
},
|
|
@@ -106,6 +106,20 @@ const modelSegment: StatusLineSegment = {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
// Show the context-window token percentage right next to the model /
|
|
110
|
+
// reasoning effort (rather than as a trailing segment) so it stays
|
|
111
|
+
// grouped with the model it describes. Disable per-preset with
|
|
112
|
+
// `segmentOptions.model.showContextPercent: false`. Suppressed
|
|
113
|
+
// automatically when a standalone context_pct segment is also in the
|
|
114
|
+
// active layout, so the value is never shown twice.
|
|
115
|
+
if (opts.showContextPercent !== false && ctx.contextPctSegmentActive !== true) {
|
|
116
|
+
const pct = ctx.contextPercent;
|
|
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)}%`)}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
109
123
|
|
|
110
124
|
return { content: theme.fg("statusLineModel", content), visible: true };
|
|
111
125
|
},
|
|
@@ -41,6 +41,12 @@ export interface SegmentContext {
|
|
|
41
41
|
};
|
|
42
42
|
contextPercent: number;
|
|
43
43
|
contextWindow: number;
|
|
44
|
+
/**
|
|
45
|
+
* True when a standalone `context_pct` segment is also part of the active
|
|
46
|
+
* layout. The model segment suppresses its inline percentage in that case to
|
|
47
|
+
* avoid showing the same value twice.
|
|
48
|
+
*/
|
|
49
|
+
contextPctSegmentActive?: boolean;
|
|
44
50
|
autoCompactEnabled: boolean;
|
|
45
51
|
subagentCount: number;
|
|
46
52
|
jobs: JobsSnapshot;
|
|
@@ -29,7 +29,7 @@ import { calculateTokensPerSecond } from "./status-line/token-rate";
|
|
|
29
29
|
import type { SeparatorDef } from "./status-line/types";
|
|
30
30
|
|
|
31
31
|
export interface StatusLineSegmentOptions {
|
|
32
|
-
model?: { showThinkingLevel?: boolean };
|
|
32
|
+
model?: { showThinkingLevel?: boolean; showContextPercent?: boolean };
|
|
33
33
|
path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
|
|
34
34
|
git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
|
|
35
35
|
time?: { format?: "12h" | "24h"; showSeconds?: boolean };
|
|
@@ -700,6 +700,11 @@ export class StatusLineComponent implements Component {
|
|
|
700
700
|
const contextTokens = breakdown.usedTokens;
|
|
701
701
|
const contextWindow = breakdown.contextWindow || state.model?.contextWindow || 0;
|
|
702
702
|
const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
703
|
+
// Suppress the inline model percentage when a standalone context_pct
|
|
704
|
+
// segment is also rendered, so the value is not shown twice.
|
|
705
|
+
const contextPctSegmentActive =
|
|
706
|
+
effectiveSettings.leftSegments.includes("context_pct") ||
|
|
707
|
+
effectiveSettings.rightSegments.includes("context_pct");
|
|
703
708
|
|
|
704
709
|
return {
|
|
705
710
|
session: this.session,
|
|
@@ -710,6 +715,7 @@ export class StatusLineComponent implements Component {
|
|
|
710
715
|
usageStats,
|
|
711
716
|
contextPercent,
|
|
712
717
|
contextWindow,
|
|
718
|
+
contextPctSegmentActive,
|
|
713
719
|
autoCompactEnabled: this.#autoCompactEnabled,
|
|
714
720
|
subagentCount: this.#subagentCount,
|
|
715
721
|
jobs: this.#jobs,
|
|
@@ -923,11 +929,8 @@ export class StatusLineComponent implements Component {
|
|
|
923
929
|
const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
|
|
924
930
|
|
|
925
931
|
if (topFillWidth > 0) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
929
|
-
}
|
|
930
|
-
// Shrink path before dropping left segments — path is the only elastic segment
|
|
932
|
+
// Shrink path before dropping right-side telemetry — path is the only elastic segment,
|
|
933
|
+
// and presets such as default-usage should not hide usage just because cwd is long.
|
|
931
934
|
const pathIdx = leftIds.indexOf("path");
|
|
932
935
|
if (pathIdx >= 0 && totalWidth() > topFillWidth) {
|
|
933
936
|
const overflow = totalWidth() - topFillWidth;
|
|
@@ -937,6 +940,10 @@ export class StatusLineComponent implements Component {
|
|
|
937
940
|
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
938
941
|
}
|
|
939
942
|
}
|
|
943
|
+
while (totalWidth() > topFillWidth && right.length > 0) {
|
|
944
|
+
right.pop();
|
|
945
|
+
rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
946
|
+
}
|
|
940
947
|
while (totalWidth() > topFillWidth && left.length > 0) {
|
|
941
948
|
left.pop();
|
|
942
949
|
leftIds.pop();
|
|
@@ -678,7 +678,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
678
678
|
imageData,
|
|
679
679
|
imageMimeType,
|
|
680
680
|
{ fallbackColor: (s: string) => theme.fg("toolOutput", s) },
|
|
681
|
-
resolveImageOptions(),
|
|
681
|
+
{ ...resolveImageOptions(), refetch: () => imageData },
|
|
682
682
|
);
|
|
683
683
|
this.#imageComponents.push(imageComponent);
|
|
684
684
|
this.addChild(imageComponent);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Component, padding, TERMINAL, truncateToWidth, visibleWidth } from "@sayknow-cli/tui";
|
|
2
2
|
import { APP_NAME } from "@sayknow-cli/utils";
|
|
3
|
+
import { formatBuildLabel } from "../../build-metadata";
|
|
3
4
|
import { t } from "../../i18n";
|
|
4
5
|
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
|
5
6
|
|
|
@@ -20,6 +21,7 @@ export interface WelcomeComponentOptions {
|
|
|
20
21
|
getReservedBottomRows?: (termWidth: number) => number;
|
|
21
22
|
changelogMarkdown?: string;
|
|
22
23
|
collapseChangelog?: boolean;
|
|
24
|
+
buildLabel?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS = 15;
|
|
@@ -249,7 +251,8 @@ export class WelcomeComponent implements Component {
|
|
|
249
251
|
const br = theme.fg("dim", theme.boxRound.bottomRight);
|
|
250
252
|
|
|
251
253
|
const lines: string[] = [];
|
|
252
|
-
const
|
|
254
|
+
const buildLabel = this.options.buildLabel ?? formatBuildLabel();
|
|
255
|
+
const title = ` ${APP_NAME} · Sayknow-CLI v${this.version} · ${buildLabel} `;
|
|
253
256
|
const titlePrefixRaw = hChar.repeat(3);
|
|
254
257
|
const titleStyled = theme.fg("dim", titlePrefixRaw) + theme.fg("muted", title);
|
|
255
258
|
const titleVisLen = visibleWidth(titlePrefixRaw) + visibleWidth(title);
|
|
@@ -567,7 +567,7 @@ export class CommandController {
|
|
|
567
567
|
`| Start a fresh session | \`${sessionNewKey}\` or \`/new\` |`,
|
|
568
568
|
"| Resume another session | `/resume` |",
|
|
569
569
|
"| Show session details | `/session info` |",
|
|
570
|
-
"| Delete current session | `/session delete` |",
|
|
570
|
+
"| Delete current session transcript/artifacts | `/session delete` |",
|
|
571
571
|
"| Select a model | `/model` or `Ctrl+L` |",
|
|
572
572
|
"| Show all shortcuts | `?` on an empty prompt or `/hotkeys` |",
|
|
573
573
|
"",
|
|
@@ -26,7 +26,7 @@ import { interruptHint } from "../shared";
|
|
|
26
26
|
import { buildAbortDisplayMessage } from "../utils/abort-message";
|
|
27
27
|
import { consumeInjectedOptimisticSignature } from "../utils/injected-user-submission";
|
|
28
28
|
import { ringTerminalBell } from "../utils/terminal-bell";
|
|
29
|
-
import { argsWithPartialJson } from "../utils/ui-helpers";
|
|
29
|
+
import { addChatChild, argsWithPartialJson } from "../utils/ui-helpers";
|
|
30
30
|
|
|
31
31
|
type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
32
32
|
|
|
@@ -169,12 +169,12 @@ export class EventController {
|
|
|
169
169
|
|
|
170
170
|
#getReadGroup(): ReadToolGroupComponent {
|
|
171
171
|
if (!this.#lastReadGroup) {
|
|
172
|
-
this.ctx
|
|
172
|
+
addChatChild(this.ctx, new Text("", 0, 0));
|
|
173
173
|
const group = new ReadToolGroupComponent({
|
|
174
174
|
showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
|
|
175
175
|
});
|
|
176
176
|
group.setExpanded(this.ctx.toolOutputExpanded);
|
|
177
|
-
this.ctx
|
|
177
|
+
addChatChild(this.ctx, group);
|
|
178
178
|
this.#lastReadGroup = group;
|
|
179
179
|
}
|
|
180
180
|
return this.#lastReadGroup;
|
|
@@ -332,7 +332,7 @@ export class EventController {
|
|
|
332
332
|
this.ctx.ui.requestRender(),
|
|
333
333
|
);
|
|
334
334
|
this.ctx.streamingMessage = event.message;
|
|
335
|
-
this.ctx
|
|
335
|
+
addChatChild(this.ctx, this.ctx.streamingComponent);
|
|
336
336
|
this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
|
|
337
337
|
this.ctx.ui.requestRender();
|
|
338
338
|
}
|
|
@@ -445,7 +445,7 @@ export class EventController {
|
|
|
445
445
|
);
|
|
446
446
|
if (!this.ctx.pendingTools.has(content.id)) {
|
|
447
447
|
this.#resetReadGroup();
|
|
448
|
-
this.ctx
|
|
448
|
+
addChatChild(this.ctx, new Text("", 0, 0));
|
|
449
449
|
const tool = this.ctx.session.getToolByName(content.name);
|
|
450
450
|
const component = new ToolExecutionComponent(
|
|
451
451
|
content.name,
|
|
@@ -462,8 +462,8 @@ export class EventController {
|
|
|
462
462
|
content.id,
|
|
463
463
|
);
|
|
464
464
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
465
|
-
this.ctx.chatContainer.addChild(component);
|
|
466
465
|
this.ctx.pendingTools.set(content.id, component);
|
|
466
|
+
addChatChild(this.ctx, component);
|
|
467
467
|
} else {
|
|
468
468
|
this.ctx.pendingTools.get(content.id)?.updateArgs(renderArgs, content.id);
|
|
469
469
|
}
|
|
@@ -575,8 +575,8 @@ export class EventController {
|
|
|
575
575
|
event.toolCallId,
|
|
576
576
|
);
|
|
577
577
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
578
|
-
this.ctx.chatContainer.addChild(component);
|
|
579
578
|
this.ctx.pendingTools.set(event.toolCallId, component);
|
|
579
|
+
addChatChild(this.ctx, component);
|
|
580
580
|
this.ctx.ui.requestRender();
|
|
581
581
|
}
|
|
582
582
|
}
|
|
@@ -869,13 +869,13 @@ export class EventController {
|
|
|
869
869
|
async #handleTtsrTriggered(event: Extract<AgentSessionEvent, { type: "ttsr_triggered" }>): Promise<void> {
|
|
870
870
|
const component = new TtsrNotificationComponent(event.rules);
|
|
871
871
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
872
|
-
this.ctx
|
|
872
|
+
addChatChild(this.ctx, component);
|
|
873
873
|
this.ctx.ui.requestRender();
|
|
874
874
|
}
|
|
875
875
|
|
|
876
876
|
async #handleTodoReminder(event: Extract<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
|
|
877
877
|
const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
|
|
878
|
-
this.ctx
|
|
878
|
+
addChatChild(this.ctx, component);
|
|
879
879
|
this.ctx.ui.requestRender();
|
|
880
880
|
}
|
|
881
881
|
|
|
@@ -18,7 +18,7 @@ import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
|
|
|
18
18
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
19
19
|
import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
|
|
20
20
|
import { resizeImage } from "../../utils/image-resize";
|
|
21
|
-
import { resolvePastedImagePath } from "../../utils/pasted-image-path";
|
|
21
|
+
import { formatPastedImageReference, resolvePastedImagePath } from "../../utils/pasted-image-path";
|
|
22
22
|
import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
|
|
23
23
|
import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
|
|
24
24
|
|
|
@@ -149,6 +149,12 @@ export class InputController {
|
|
|
149
149
|
return { consume: true };
|
|
150
150
|
}
|
|
151
151
|
const hookDialogActive = this.#hasHookDialog();
|
|
152
|
+
if (this.ctx.hookSelector?.hasActiveInlineInput?.() === true) {
|
|
153
|
+
// Inline ask/custom-input editors use Esc to return to the option list.
|
|
154
|
+
// Let the focused selector see the key instead of converting a typo
|
|
155
|
+
// into a full workflow/session abort while the agent is streaming.
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
152
158
|
if (
|
|
153
159
|
this.#handleCancellableWorkEscape({
|
|
154
160
|
loading: hookDialogActive,
|
|
@@ -1096,7 +1102,7 @@ export class InputController {
|
|
|
1096
1102
|
data: image.data,
|
|
1097
1103
|
mimeType: image.mimeType,
|
|
1098
1104
|
});
|
|
1099
|
-
this.ctx.editor.insertText(`${this.#nextImagePlaceholder()} `);
|
|
1105
|
+
this.ctx.editor.insertText(`${formatPastedImageReference(this.#nextImagePlaceholder(), image.resolvedPath)} `);
|
|
1100
1106
|
this.ctx.showStatus(`Attached image: ${path.basename(image.resolvedPath)}`, { dim: true });
|
|
1101
1107
|
this.ctx.ui.requestRender();
|
|
1102
1108
|
return true;
|
|
@@ -1333,8 +1333,12 @@ export class SelectorController {
|
|
|
1333
1333
|
}
|
|
1334
1334
|
|
|
1335
1335
|
const confirmed = await this.ctx.showHookConfirm(
|
|
1336
|
-
"Delete
|
|
1337
|
-
|
|
1336
|
+
"Delete current session transcript and artifacts?",
|
|
1337
|
+
[
|
|
1338
|
+
"This permanently deletes only the current session transcript file and its artifacts directory.",
|
|
1339
|
+
"Other sessions and topic/history metadata are not deleted.",
|
|
1340
|
+
"You will be moved to a fresh session and returned to the session selector.",
|
|
1341
|
+
].join("\n"),
|
|
1338
1342
|
);
|
|
1339
1343
|
|
|
1340
1344
|
if (!confirmed) {
|
|
@@ -1351,7 +1355,7 @@ export class SelectorController {
|
|
|
1351
1355
|
await storage.deleteSessionWithArtifacts(sessionFile);
|
|
1352
1356
|
|
|
1353
1357
|
// Show session selector
|
|
1354
|
-
this.ctx.showStatus("
|
|
1358
|
+
this.ctx.showStatus("Current session transcript and artifacts deleted");
|
|
1355
1359
|
await this.showSessionSelector();
|
|
1356
1360
|
}
|
|
1357
1361
|
|
|
@@ -18,6 +18,7 @@ import type { Component, EditorTheme, SlashCommand } from "@sayknow-cli/tui";
|
|
|
18
18
|
import {
|
|
19
19
|
Container,
|
|
20
20
|
clearRenderCache,
|
|
21
|
+
getRenderCacheRetainedBytes,
|
|
21
22
|
Loader,
|
|
22
23
|
Markdown,
|
|
23
24
|
ProcessTerminal,
|
|
@@ -115,7 +116,7 @@ import {
|
|
|
115
116
|
theme,
|
|
116
117
|
} from "./theme/theme";
|
|
117
118
|
import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInput, TodoItem, TodoPhase } from "./types";
|
|
118
|
-
import { UiHelpers } from "./utils/ui-helpers";
|
|
119
|
+
import { addChatChild, UiHelpers } from "./utils/ui-helpers";
|
|
119
120
|
|
|
120
121
|
const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
|
|
121
122
|
const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
|
|
@@ -399,6 +400,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
399
400
|
) {
|
|
400
401
|
this.session = session;
|
|
401
402
|
this.sessionManager = session.sessionManager;
|
|
403
|
+
this.session.setRetainedMemorySampler(() => ({
|
|
404
|
+
tuiChatChildren: this.chatContainer.children.length,
|
|
405
|
+
tuiCachedRenderBytes: getRenderCacheRetainedBytes(),
|
|
406
|
+
}));
|
|
402
407
|
this.settings = session.settings;
|
|
403
408
|
this.keybindings = KeybindingsManager.inMemory();
|
|
404
409
|
this.agent = session.agent;
|
|
@@ -1515,8 +1520,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1515
1520
|
|
|
1516
1521
|
#renderPlanPreview(planContent: string, options?: { append?: boolean }): void {
|
|
1517
1522
|
const existingContainer = this.#planReviewContainer;
|
|
1518
|
-
|
|
1519
|
-
|
|
1523
|
+
// Only reuse the existing preview when it is still attached to the chat container;
|
|
1524
|
+
// a collapsed/disposed container must be re-added, not mutated in place.
|
|
1525
|
+
const stillAttached = existingContainer !== undefined && this.chatContainer.children.includes(existingContainer);
|
|
1526
|
+
const replaceExisting = options?.append !== true && stillAttached;
|
|
1527
|
+
const planReviewContainer = replaceExisting && existingContainer ? existingContainer : new Container();
|
|
1520
1528
|
planReviewContainer.clear();
|
|
1521
1529
|
planReviewContainer.addChild(new Spacer(1));
|
|
1522
1530
|
planReviewContainer.addChild(new DynamicBorder());
|
|
@@ -1525,7 +1533,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1525
1533
|
planReviewContainer.addChild(new Markdown(planContent, 1, 1, getMarkdownTheme()));
|
|
1526
1534
|
planReviewContainer.addChild(new DynamicBorder());
|
|
1527
1535
|
if (!replaceExisting) {
|
|
1528
|
-
this
|
|
1536
|
+
addChatChild(this, planReviewContainer);
|
|
1529
1537
|
}
|
|
1530
1538
|
this.#planReviewContainer = planReviewContainer;
|
|
1531
1539
|
this.ui.requestRender();
|