@gajae-code/coding-agent 0.11.10 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +86 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +18 -2
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
- package/dist/types/sdk/host/control/operations.d.ts +2 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +51 -19
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +59 -13
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/skill-state/active-state.d.ts +0 -4
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/package.json +9 -13
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +8 -0
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +49 -2
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +91 -7
- package/src/commands/team.ts +1 -1
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +24 -0
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +84 -3
- package/src/config/settings.ts +254 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -921
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +979 -71
- package/src/gjc-runtime/state-runtime.ts +50 -12
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +34 -5
- package/src/gjc-runtime/team-runtime.ts +970 -33
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
- package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
- package/src/gjc-runtime/workflow-manifest.ts +34 -179
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +10 -11
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +47 -0
- package/src/modes/acp/acp-agent.ts +597 -67
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +76 -5
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +62 -6
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +127 -29
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/lifecycle.ts +107 -7
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +503 -60
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
- package/src/sdk/bus/telegram-daemon.ts +531 -53
- package/src/sdk/host/control/dispatch.ts +7 -1
- package/src/sdk/host/control/operations.ts +2 -1
- package/src/sdk/host/host.ts +25 -3
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +76 -0
- package/src/sdk/protocol/operation-registry.ts +8 -2
- package/src/sdk/session.ts +25 -2
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +720 -190
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +40 -10
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +1372 -42
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +0 -10
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +65 -7
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +2 -0
- package/src/task/types.ts +22 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +89 -69
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1542 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Container, matchesKey, type SelectItem, SelectList, Spacer, Text } from "@gajae-code/tui";
|
|
2
|
+
import { formatKeyHint } from "../../config/keybindings";
|
|
2
3
|
import type { QueuedMessageEditEntry } from "../../session/agent-session";
|
|
3
4
|
import { getSelectListTheme, theme } from "../theme/theme";
|
|
4
5
|
import { DynamicBorder } from "./dynamic-border";
|
|
5
6
|
|
|
6
7
|
const MAX_VISIBLE_QUEUE_MESSAGES = 8;
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
type QueueSelectorAction = "tui.select.confirm" | "tui.select.cancel";
|
|
9
|
+
type QueueSelectorNavigationAction = "tui.select.up" | "tui.select.down" | "tui.select.pageUp" | "tui.select.pageDown";
|
|
9
10
|
|
|
10
11
|
export class QueuePaneComponent extends Container {
|
|
11
12
|
#selectList: SelectList;
|
|
@@ -13,11 +14,20 @@ export class QueuePaneComponent extends Container {
|
|
|
13
14
|
#selectedIndex: number;
|
|
14
15
|
#onDelete: (entry: QueuedMessageEditEntry, index: number) => void;
|
|
15
16
|
#onMove: (entry: QueuedMessageEditEntry, index: number, direction: "up" | "down") => void;
|
|
16
|
-
|
|
17
|
+
#onSelect: (entry: QueuedMessageEditEntry) => void;
|
|
18
|
+
#onClose: () => void;
|
|
19
|
+
#entries: QueuedMessageEditEntry[];
|
|
20
|
+
#matchesSelectAction: (keyData: string, action: QueueSelectorAction) => boolean;
|
|
21
|
+
#resolveSelectNavigation: (keyData: string) => QueueSelectorNavigationAction | undefined;
|
|
17
22
|
constructor(
|
|
18
23
|
entries: QueuedMessageEditEntry[],
|
|
19
24
|
options: {
|
|
20
25
|
selectedIndex?: number;
|
|
26
|
+
formatKeyHint?: (key: string) => string;
|
|
27
|
+
formatSelectAction?: (action: QueueSelectorAction) => string;
|
|
28
|
+
matchesSelectAction?: (keyData: string, action: QueueSelectorAction) => boolean;
|
|
29
|
+
resolveSelectNavigation?: (keyData: string) => QueueSelectorNavigationAction | undefined;
|
|
30
|
+
onSelect: (entry: QueuedMessageEditEntry) => void;
|
|
21
31
|
onDelete: (entry: QueuedMessageEditEntry, index: number) => void;
|
|
22
32
|
onMove: (entry: QueuedMessageEditEntry, index: number, direction: "up" | "down") => void;
|
|
23
33
|
onClose: () => void;
|
|
@@ -25,23 +35,46 @@ export class QueuePaneComponent extends Container {
|
|
|
25
35
|
) {
|
|
26
36
|
super();
|
|
27
37
|
this.#onDelete = options.onDelete;
|
|
38
|
+
this.#onSelect = options.onSelect;
|
|
28
39
|
this.#onMove = options.onMove;
|
|
40
|
+
this.#onClose = options.onClose;
|
|
41
|
+
this.#entries = entries;
|
|
42
|
+
this.#matchesSelectAction =
|
|
43
|
+
options.matchesSelectAction ??
|
|
44
|
+
((keyData, action) => matchesKey(keyData, action === "tui.select.confirm" ? "enter" : "escape"));
|
|
45
|
+
this.#resolveSelectNavigation = options.resolveSelectNavigation ?? (() => undefined);
|
|
29
46
|
this.#selectedIndex = Math.max(0, Math.min(options.selectedIndex ?? 0, entries.length - 1));
|
|
30
47
|
this.#selectedEntry = entries[this.#selectedIndex];
|
|
31
48
|
const byId = new Map(entries.map(entry => [entry.id, entry]));
|
|
49
|
+
const displayKey = options.formatKeyHint ?? formatKeyHint;
|
|
50
|
+
const selectKeys = `${displayKey("alt+up")}/${displayKey("alt+down")}`;
|
|
51
|
+
const editKey = options.formatSelectAction
|
|
52
|
+
? options.formatSelectAction("tui.select.confirm") || "Disabled"
|
|
53
|
+
: displayKey("enter");
|
|
54
|
+
const deleteKey = displayKey("delete");
|
|
55
|
+
const moveKeys = `${displayKey("ctrl+up")}/${displayKey("ctrl+down")}`;
|
|
56
|
+
const closeKey = options.formatSelectAction
|
|
57
|
+
? options.formatSelectAction("tui.select.cancel") || "Disabled"
|
|
58
|
+
: displayKey("escape");
|
|
59
|
+
const itemHint = `${editKey} edit · ${deleteKey} remove · ${moveKeys} move`;
|
|
60
|
+
const controlsHint = `${selectKeys} select · ${itemHint} · ${closeKey} close`;
|
|
32
61
|
const items: SelectItem[] = entries.map((entry, index) => ({
|
|
33
62
|
value: entry.id,
|
|
34
63
|
label: `${entry.label} ${index + 1}`,
|
|
35
64
|
description: entry.text,
|
|
36
|
-
hint:
|
|
65
|
+
hint: itemHint,
|
|
37
66
|
}));
|
|
38
67
|
|
|
39
68
|
this.addChild(new Spacer(1));
|
|
40
69
|
this.addChild(new Text(theme.bold("Message queue"), 1, 0));
|
|
41
|
-
this.addChild(new Text(theme.fg("muted",
|
|
70
|
+
this.addChild(new Text(theme.fg("muted", controlsHint), 1, 0));
|
|
42
71
|
this.addChild(new Spacer(1));
|
|
43
72
|
this.addChild(new DynamicBorder());
|
|
44
73
|
this.#selectList = new SelectList(items, MAX_VISIBLE_QUEUE_MESSAGES, getSelectListTheme());
|
|
74
|
+
this.#selectList.onSelect = item => {
|
|
75
|
+
const entry = byId.get(item.value);
|
|
76
|
+
if (entry) this.#onSelect(entry);
|
|
77
|
+
};
|
|
45
78
|
this.#selectList.onSelectionChange = item => {
|
|
46
79
|
const index = entries.findIndex(entry => entry.id === item.value);
|
|
47
80
|
this.#selectedIndex = index === -1 ? this.#selectedIndex : index;
|
|
@@ -54,12 +87,25 @@ export class QueuePaneComponent extends Container {
|
|
|
54
87
|
}
|
|
55
88
|
|
|
56
89
|
handleInput(keyData: string): void {
|
|
57
|
-
if (
|
|
58
|
-
this.#
|
|
90
|
+
if (this.#matchesSelectAction(keyData, "tui.select.confirm")) {
|
|
91
|
+
if (this.#selectedEntry) this.#onSelect(this.#selectedEntry);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (this.#matchesSelectAction(keyData, "tui.select.cancel")) {
|
|
95
|
+
this.#onClose();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const navigation = this.#resolveSelectNavigation(keyData);
|
|
99
|
+
if (navigation) {
|
|
100
|
+
this.#selectList.handleNavigation(navigation);
|
|
59
101
|
return;
|
|
60
102
|
}
|
|
61
|
-
if (matchesKey(keyData, "
|
|
62
|
-
|
|
103
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "escape")) return;
|
|
104
|
+
if (matchesKey(keyData, "alt+up") || matchesKey(keyData, "alt+down")) {
|
|
105
|
+
const direction = matchesKey(keyData, "alt+up") ? -1 : 1;
|
|
106
|
+
this.#selectedIndex = (this.#selectedIndex + direction + this.#entries.length) % this.#entries.length;
|
|
107
|
+
this.#selectedEntry = this.#entries[this.#selectedIndex];
|
|
108
|
+
this.#selectList.setSelectedIndex(this.#selectedIndex);
|
|
63
109
|
return;
|
|
64
110
|
}
|
|
65
111
|
if (matchesKey(keyData, "ctrl+up") || matchesKey(keyData, "ctrl+shift+up")) {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Container, matchesKey, type SelectItem, SelectList, Spacer, Text } from "@gajae-code/tui";
|
|
2
|
+
import { formatKeyHint } from "../../config/keybindings";
|
|
2
3
|
import type { QueuedMessageEditEntry } from "../../session/agent-session";
|
|
3
4
|
import { getSelectListTheme, theme } from "../theme/theme";
|
|
4
5
|
import { DynamicBorder } from "./dynamic-border";
|
|
5
6
|
|
|
6
7
|
const MAX_VISIBLE_QUEUED_MESSAGES = 8;
|
|
7
|
-
const RAW_UP = "\x1b[A";
|
|
8
|
-
const RAW_DOWN = "\x1b[B";
|
|
9
8
|
type QueuedMessageMoveDirection = "up" | "down";
|
|
9
|
+
type QueueSelectorAction = "tui.select.confirm" | "tui.select.cancel";
|
|
10
|
+
type QueueSelectorNavigationAction = "tui.select.up" | "tui.select.down" | "tui.select.pageUp" | "tui.select.pageDown";
|
|
10
11
|
|
|
11
12
|
export type { QueuedMessageMoveDirection };
|
|
12
13
|
|
|
@@ -16,32 +17,61 @@ export class QueuedMessageSelectorComponent extends Container {
|
|
|
16
17
|
#selectedIndex = 0;
|
|
17
18
|
#onDelete: (entry: QueuedMessageEditEntry, selectedIndex: number) => void;
|
|
18
19
|
#onMove: (entry: QueuedMessageEditEntry, selectedIndex: number, direction: QueuedMessageMoveDirection) => void;
|
|
19
|
-
|
|
20
|
+
#onSelect: (entry: QueuedMessageEditEntry) => void;
|
|
21
|
+
#onCancel: () => void;
|
|
22
|
+
#entries: QueuedMessageEditEntry[];
|
|
23
|
+
#matchesSelectAction: (keyData: string, action: QueueSelectorAction) => boolean;
|
|
24
|
+
#resolveSelectNavigation: (keyData: string) => QueueSelectorNavigationAction | undefined;
|
|
20
25
|
constructor(
|
|
21
26
|
entries: QueuedMessageEditEntry[],
|
|
22
27
|
onSelect: (entry: QueuedMessageEditEntry) => void,
|
|
23
28
|
onDelete: (entry: QueuedMessageEditEntry, selectedIndex: number) => void,
|
|
24
29
|
onMove: (entry: QueuedMessageEditEntry, selectedIndex: number, direction: QueuedMessageMoveDirection) => void,
|
|
25
30
|
onCancel: () => void,
|
|
26
|
-
options?: {
|
|
31
|
+
options?: {
|
|
32
|
+
selectedIndex?: number;
|
|
33
|
+
formatKeyHint?: (key: string) => string;
|
|
34
|
+
formatSelectAction?: (action: QueueSelectorAction) => string;
|
|
35
|
+
matchesSelectAction?: (keyData: string, action: QueueSelectorAction) => boolean;
|
|
36
|
+
resolveSelectNavigation?: (keyData: string) => QueueSelectorNavigationAction | undefined;
|
|
37
|
+
},
|
|
27
38
|
) {
|
|
28
39
|
super();
|
|
29
40
|
|
|
30
41
|
this.#onDelete = onDelete;
|
|
31
42
|
this.#onMove = onMove;
|
|
43
|
+
this.#onSelect = onSelect;
|
|
44
|
+
this.#onCancel = onCancel;
|
|
45
|
+
this.#entries = entries;
|
|
46
|
+
this.#matchesSelectAction =
|
|
47
|
+
options?.matchesSelectAction ??
|
|
48
|
+
((keyData, action) => matchesKey(keyData, action === "tui.select.confirm" ? "enter" : "escape"));
|
|
49
|
+
this.#resolveSelectNavigation = options?.resolveSelectNavigation ?? (() => undefined);
|
|
32
50
|
const byId = new Map(entries.map(entry => [entry.id, entry]));
|
|
33
51
|
this.#selectedIndex = Math.max(0, Math.min(options?.selectedIndex ?? 0, entries.length - 1));
|
|
34
52
|
this.#selectedEntry = entries[this.#selectedIndex];
|
|
53
|
+
const displayKey = options?.formatKeyHint ?? formatKeyHint;
|
|
54
|
+
const selectKeys = `${displayKey("alt+up")}/${displayKey("alt+down")}`;
|
|
55
|
+
const editKey = options?.formatSelectAction
|
|
56
|
+
? options.formatSelectAction("tui.select.confirm") || "Disabled"
|
|
57
|
+
: displayKey("enter");
|
|
58
|
+
const deleteKey = displayKey("delete");
|
|
59
|
+
const moveKeys = `${displayKey("ctrl+up")}/${displayKey("ctrl+down")}`;
|
|
60
|
+
const cancelKey = options?.formatSelectAction
|
|
61
|
+
? options.formatSelectAction("tui.select.cancel") || "Disabled"
|
|
62
|
+
: displayKey("escape");
|
|
63
|
+
const itemHint = `${editKey} edit · ${deleteKey} remove · ${moveKeys} move`;
|
|
64
|
+
const controlsHint = `${selectKeys} select · ${itemHint} · ${cancelKey} cancel`;
|
|
35
65
|
const items: SelectItem[] = entries.map((entry, index) => ({
|
|
36
66
|
value: entry.id,
|
|
37
67
|
label: `${entry.label} ${index + 1}`,
|
|
38
68
|
description: entry.text,
|
|
39
|
-
hint:
|
|
69
|
+
hint: itemHint,
|
|
40
70
|
}));
|
|
41
71
|
|
|
42
72
|
this.addChild(new Spacer(1));
|
|
43
73
|
this.addChild(new Text(theme.bold("Queued messages"), 1, 0));
|
|
44
|
-
this.addChild(new Text(theme.fg("muted",
|
|
74
|
+
this.addChild(new Text(theme.fg("muted", controlsHint), 1, 0));
|
|
45
75
|
this.addChild(new Spacer(1));
|
|
46
76
|
this.addChild(new DynamicBorder());
|
|
47
77
|
|
|
@@ -63,12 +93,25 @@ export class QueuedMessageSelectorComponent extends Container {
|
|
|
63
93
|
}
|
|
64
94
|
|
|
65
95
|
handleInput(keyData: string): void {
|
|
66
|
-
if (
|
|
67
|
-
this.#
|
|
96
|
+
if (this.#matchesSelectAction(keyData, "tui.select.confirm")) {
|
|
97
|
+
if (this.#selectedEntry) this.#onSelect(this.#selectedEntry);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (this.#matchesSelectAction(keyData, "tui.select.cancel")) {
|
|
101
|
+
this.#onCancel();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const navigation = this.#resolveSelectNavigation(keyData);
|
|
105
|
+
if (navigation) {
|
|
106
|
+
this.#selectList.handleNavigation(navigation);
|
|
68
107
|
return;
|
|
69
108
|
}
|
|
70
|
-
if (matchesKey(keyData, "
|
|
71
|
-
|
|
109
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "escape")) return;
|
|
110
|
+
if (matchesKey(keyData, "alt+up") || matchesKey(keyData, "alt+down")) {
|
|
111
|
+
const direction = matchesKey(keyData, "alt+up") ? -1 : 1;
|
|
112
|
+
this.#selectedIndex = (this.#selectedIndex + direction + this.#entries.length) % this.#entries.length;
|
|
113
|
+
this.#selectedEntry = this.#entries[this.#selectedIndex];
|
|
114
|
+
this.#selectList.setSelectedIndex(this.#selectedIndex);
|
|
72
115
|
return;
|
|
73
116
|
}
|
|
74
117
|
if (matchesKey(keyData, "ctrl+up") || matchesKey(keyData, "ctrl+shift+up")) {
|
|
@@ -3,7 +3,7 @@ import { Container, Text } from "@gajae-code/tui";
|
|
|
3
3
|
import { InternalUrlRouter } from "../../internal-urls";
|
|
4
4
|
import { getLanguageFromPath, theme } from "../../modes/theme/theme";
|
|
5
5
|
import { splitPathAndSel } from "../../tools/path-utils";
|
|
6
|
-
import { PREVIEW_LIMITS, shortenPath } from "../../tools/render-utils";
|
|
6
|
+
import { PREVIEW_LIMITS, replaceTabs, shortenPath } from "../../tools/render-utils";
|
|
7
7
|
import { renderCodeCell } from "../../tui";
|
|
8
8
|
import type { ToolExecutionHandle } from "./tool-execution";
|
|
9
9
|
|
|
@@ -53,9 +53,16 @@ type ReadToolResultDetails = {
|
|
|
53
53
|
conflictCount?: number;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
type
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
type ReadToolVisibleProjection = ReadonlyArray<{
|
|
57
|
+
path: string;
|
|
58
|
+
status: ReadEntry["status"];
|
|
59
|
+
correctedFrom?: string;
|
|
60
|
+
conflictCount?: number;
|
|
61
|
+
preview?: {
|
|
62
|
+
lines: readonly string[];
|
|
63
|
+
hiddenLineCount: number;
|
|
64
|
+
};
|
|
65
|
+
}>;
|
|
59
66
|
|
|
60
67
|
function getSuffixResolution(details: ReadToolResultDetails | undefined): ReadToolSuffixResolution | undefined {
|
|
61
68
|
if (typeof details?.suffixResolution?.from !== "string" || typeof details.suffixResolution.to !== "string") {
|
|
@@ -82,13 +89,16 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
82
89
|
#expanded = false;
|
|
83
90
|
#manuallyExpanded: boolean | undefined;
|
|
84
91
|
#showContentPreview: boolean;
|
|
92
|
+
#visibleTranscriptChanged = false;
|
|
93
|
+
#lastVisibleProjection: ReadToolVisibleProjection = [];
|
|
85
94
|
|
|
86
|
-
constructor(options:
|
|
95
|
+
constructor(options: { showContentPreview?: boolean } = {}) {
|
|
87
96
|
super();
|
|
88
97
|
this.#showContentPreview = options.showContentPreview ?? false;
|
|
89
98
|
this.#text = new Text("", 0, 0);
|
|
90
99
|
this.addChild(this.#text);
|
|
91
100
|
this.#updateDisplay();
|
|
101
|
+
this.#lastVisibleProjection = this.#visibleProjection();
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
updateArgs(args: ReadRenderArgs, toolCallId?: string): void {
|
|
@@ -102,7 +112,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
102
112
|
};
|
|
103
113
|
entry.path = rawPath;
|
|
104
114
|
this.#entries.set(toolCallId, entry);
|
|
105
|
-
this.#
|
|
115
|
+
this.#updateDisplayAndNotify();
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
updateResult(
|
|
@@ -126,23 +136,20 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
126
136
|
typeof details?.conflictCount === "number" && details.conflictCount > 0 ? details.conflictCount : undefined;
|
|
127
137
|
entry.conflictCount = conflictCount;
|
|
128
138
|
entry.status = result.isError ? "error" : suffixResolution ? "warning" : "success";
|
|
129
|
-
// Store the text content for preview/expanded display
|
|
130
139
|
const textContent = result.content?.find(c => c.type === "text")?.text;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
this.#updateDisplay();
|
|
140
|
+
entry.contentText = textContent;
|
|
141
|
+
this.#updateDisplayAndNotify();
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
setArgsComplete(_toolCallId?: string): void {
|
|
138
|
-
this.#
|
|
145
|
+
this.#updateDisplayAndNotify();
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
/** Applies automatic expansion unless this renderer instance has an explicit fold choice. */
|
|
142
149
|
setExpanded(expanded: boolean): void {
|
|
143
150
|
if (this.#manuallyExpanded !== undefined) return;
|
|
144
151
|
this.#expanded = expanded;
|
|
145
|
-
this.#
|
|
152
|
+
this.#updateDisplayAndNotify();
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
/**
|
|
@@ -152,13 +159,74 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
152
159
|
setManuallyExpanded(expanded: boolean): void {
|
|
153
160
|
this.#manuallyExpanded = expanded;
|
|
154
161
|
this.#expanded = expanded;
|
|
155
|
-
this.#
|
|
162
|
+
this.#updateDisplayAndNotify();
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
getComponent(): Component {
|
|
159
166
|
return this;
|
|
160
167
|
}
|
|
161
168
|
|
|
169
|
+
consumeVisibleTranscriptChange(): boolean {
|
|
170
|
+
const changed = this.#visibleTranscriptChanged;
|
|
171
|
+
this.#visibleTranscriptChanged = false;
|
|
172
|
+
return changed;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#visibleProjection(): ReadToolVisibleProjection {
|
|
176
|
+
return [...this.#entries.values()].map(entry => {
|
|
177
|
+
if (!this.#shouldRenderPreview(entry)) {
|
|
178
|
+
return {
|
|
179
|
+
path: entry.path,
|
|
180
|
+
status: entry.status,
|
|
181
|
+
correctedFrom: entry.correctedFrom,
|
|
182
|
+
conflictCount: entry.conflictCount,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const lines = replaceTabs(entry.contentText ?? "")
|
|
186
|
+
.split(/\r?\n/)
|
|
187
|
+
.map(line => line.slice(line.lastIndexOf("\r") + 1));
|
|
188
|
+
const visibleLineCount = this.#expanded ? lines.length : Math.min(lines.length, COLLAPSED_PREVIEW_LINES);
|
|
189
|
+
return {
|
|
190
|
+
path: entry.path,
|
|
191
|
+
status: entry.status,
|
|
192
|
+
correctedFrom: entry.correctedFrom,
|
|
193
|
+
preview: {
|
|
194
|
+
lines: lines.slice(0, visibleLineCount),
|
|
195
|
+
hiddenLineCount: lines.length - visibleLineCount,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#sameVisibleProjection(left: ReadToolVisibleProjection, right: ReadToolVisibleProjection): boolean {
|
|
202
|
+
if (left.length !== right.length) return false;
|
|
203
|
+
return left.every((entry, index) => {
|
|
204
|
+
const other = right[index];
|
|
205
|
+
if (
|
|
206
|
+
!other ||
|
|
207
|
+
entry.path !== other.path ||
|
|
208
|
+
entry.status !== other.status ||
|
|
209
|
+
entry.correctedFrom !== other.correctedFrom ||
|
|
210
|
+
(!entry.preview && entry.conflictCount !== other.conflictCount)
|
|
211
|
+
)
|
|
212
|
+
return false;
|
|
213
|
+
if (!entry.preview || !other.preview) return entry.preview === other.preview;
|
|
214
|
+
return (
|
|
215
|
+
entry.preview.hiddenLineCount === other.preview.hiddenLineCount &&
|
|
216
|
+
entry.preview.lines.length === other.preview.lines.length &&
|
|
217
|
+
entry.preview.lines.every((line, lineIndex) => line === other.preview!.lines[lineIndex])
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#updateDisplayAndNotify(): void {
|
|
223
|
+
this.#updateDisplay();
|
|
224
|
+
const after = this.#visibleProjection();
|
|
225
|
+
if (this.#sameVisibleProjection(after, this.#lastVisibleProjection)) return;
|
|
226
|
+
this.#lastVisibleProjection = after;
|
|
227
|
+
this.#visibleTranscriptChanged = true;
|
|
228
|
+
}
|
|
229
|
+
|
|
162
230
|
#updateDisplay(): void {
|
|
163
231
|
const entries = [...this.#entries.values()];
|
|
164
232
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import type { ToolResultMessage } from "@gajae-code/ai";
|
|
3
4
|
import { matchesKey } from "@gajae-code/tui";
|
|
4
5
|
import { formatDuration, formatNumber } from "@gajae-code/utils";
|
|
5
6
|
import type { KeyId } from "../../config/keybindings";
|
|
6
7
|
import { isSilentAbort } from "../../session/messages";
|
|
7
|
-
import type { SessionMessageEntry } from "../../session/session-manager";
|
|
8
|
+
import type { FileEntry, SessionMessageEntry } from "../../session/session-manager";
|
|
8
9
|
import { parseSessionEntries } from "../../session/session-manager";
|
|
9
10
|
import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
|
|
10
11
|
import { theme } from "../theme/theme";
|
|
@@ -15,13 +16,26 @@ import {
|
|
|
15
16
|
} from "./tool-transcript-format";
|
|
16
17
|
import { type TranscriptViewerEntry, TranscriptViewerOverlay } from "./transcript-viewer-overlay";
|
|
17
18
|
|
|
19
|
+
const FATAL_UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
20
|
+
|
|
21
|
+
type FileIdentity = { dev: bigint; ino: bigint };
|
|
22
|
+
type StableSnapshot = { identity: FileIdentity; size: number; prefix: Buffer; tail: Buffer };
|
|
23
|
+
type ObserverCache = {
|
|
24
|
+
path: string;
|
|
25
|
+
identity: FileIdentity;
|
|
26
|
+
completeOffset: number;
|
|
27
|
+
prefixDigest: string;
|
|
28
|
+
entries: SessionMessageEntry[];
|
|
29
|
+
model?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
18
32
|
/** Session-observer adapter. The shared viewer owns navigation and fold state. */
|
|
19
33
|
export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
|
|
20
34
|
#registry: SessionObserverRegistry;
|
|
21
35
|
#onDone: () => void;
|
|
22
36
|
#observeKeys: readonly KeyId[];
|
|
23
37
|
#selectedSessionId?: string;
|
|
24
|
-
#cache?:
|
|
38
|
+
#cache?: ObserverCache;
|
|
25
39
|
|
|
26
40
|
constructor(registry: SessionObserverRegistry, onDone: () => void, observeKeys: KeyId[]) {
|
|
27
41
|
// The option closures run during the base constructor's initial refresh,
|
|
@@ -126,32 +140,81 @@ export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
|
|
|
126
140
|
return stats.length ? [theme.fg("dim", stats.join(theme.sep.dot))] : [];
|
|
127
141
|
}
|
|
128
142
|
#load(filePath: string): SessionMessageEntry[] {
|
|
129
|
-
if (this.#cache
|
|
130
|
-
const fromByte = this.#cache?.bytesRead ?? 0;
|
|
131
|
-
const result = readFileIncremental(filePath, fromByte);
|
|
132
|
-
if (!result) return this.#cache?.entries ?? [];
|
|
133
|
-
if (result.newSize < fromByte) {
|
|
143
|
+
if (this.#cache && this.#cache.path !== filePath) {
|
|
134
144
|
this.#cache = undefined;
|
|
135
|
-
|
|
145
|
+
this.resetSourceState();
|
|
136
146
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
|
|
148
|
+
const cache = this.#cache;
|
|
149
|
+
const snapshot = readStableSnapshot(filePath, cache?.completeOffset ?? 0);
|
|
150
|
+
if (!snapshot) {
|
|
151
|
+
// An unreadable source is never allowed to keep presenting a prior file as current.
|
|
152
|
+
if (cache) this.#clearSource();
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const canAppend =
|
|
157
|
+
cache &&
|
|
158
|
+
sameIdentity(cache.identity, snapshot.identity) &&
|
|
159
|
+
snapshot.size >= cache.completeOffset &&
|
|
160
|
+
digest(snapshot.prefix) === cache.prefixDigest;
|
|
161
|
+
if (canAppend) return this.#appendStable(cache, snapshot);
|
|
162
|
+
|
|
163
|
+
// A changed fd identity, changed committed bytes, or a shrink is a replacement.
|
|
164
|
+
// Validate the entire candidate before it replaces the visible transcript.
|
|
165
|
+
const replacement = readStableSnapshot(filePath, 0);
|
|
166
|
+
if (!replacement) {
|
|
167
|
+
this.#clearSource();
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const candidate = cacheEntries(filePath, replacement);
|
|
171
|
+
if (!candidate) {
|
|
172
|
+
this.#clearSource();
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
this.#cache = candidate;
|
|
176
|
+
if (cache) this.resetSourceState();
|
|
177
|
+
return candidate.entries;
|
|
178
|
+
}
|
|
179
|
+
#appendStable(cache: ObserverCache, snapshot: StableSnapshot): SessionMessageEntry[] {
|
|
180
|
+
const complete = completePrefix(snapshot.tail);
|
|
181
|
+
if (!complete) {
|
|
182
|
+
if (!isValidUtf8Prefix(snapshot.tail)) {
|
|
183
|
+
this.#clearSource();
|
|
184
|
+
return [];
|
|
149
185
|
}
|
|
186
|
+
return cache.entries;
|
|
187
|
+
}
|
|
188
|
+
const remainder = snapshot.tail.subarray(complete.bytes.length);
|
|
189
|
+
if (!isValidUtf8Prefix(remainder)) {
|
|
190
|
+
this.#clearSource();
|
|
191
|
+
return [];
|
|
150
192
|
}
|
|
151
|
-
|
|
193
|
+
const parsed = parseCompleteEntries(complete.bytes);
|
|
194
|
+
if (!parsed) {
|
|
195
|
+
this.#clearSource();
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const entries = [...cache.entries, ...parsed.entries];
|
|
199
|
+
this.#cache = {
|
|
200
|
+
...cache,
|
|
201
|
+
completeOffset: cache.completeOffset + complete.bytes.length,
|
|
202
|
+
prefixDigest: digest(Buffer.concat([snapshot.prefix, complete.bytes])),
|
|
203
|
+
entries,
|
|
204
|
+
model: parsed.model ?? cache.model,
|
|
205
|
+
};
|
|
206
|
+
return entries;
|
|
207
|
+
}
|
|
208
|
+
#clearSource(): void {
|
|
209
|
+
if (this.#cache) this.resetSourceState();
|
|
210
|
+
this.#cache = undefined;
|
|
152
211
|
}
|
|
153
212
|
}
|
|
154
213
|
|
|
214
|
+
/**
|
|
215
|
+
* This deliberately remains the eager full-history projection. PR2 owns projection
|
|
216
|
+
* virtualization/incrementalization; source acquisition above only controls snapshot safety.
|
|
217
|
+
*/
|
|
155
218
|
export function entriesFromMessages(entries: readonly SessionMessageEntry[]): TranscriptViewerEntry[] {
|
|
156
219
|
const results = new Map<string, ToolResultMessage>();
|
|
157
220
|
for (const entry of entries)
|
|
@@ -259,19 +322,111 @@ function truncateThinking(text: string, expanded: boolean): string {
|
|
|
259
322
|
return text.length > limit ? `${text.slice(0, limit)}...` : text;
|
|
260
323
|
}
|
|
261
324
|
|
|
262
|
-
function
|
|
325
|
+
function readStableSnapshot(filePath: string, completeOffset: number): StableSnapshot | null {
|
|
326
|
+
let fd: number | undefined;
|
|
263
327
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
328
|
+
fd = fs.openSync(filePath, "r");
|
|
329
|
+
const before = fs.fstatSync(fd, { bigint: true });
|
|
330
|
+
if (before.size > BigInt(Number.MAX_SAFE_INTEGER) || completeOffset > Number(before.size)) return null;
|
|
331
|
+
const size = Number(before.size);
|
|
332
|
+
const prefix = readExactly(fd, 0, completeOffset);
|
|
333
|
+
const tail = readExactly(fd, completeOffset, size - completeOffset);
|
|
334
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
335
|
+
if (
|
|
336
|
+
before.dev !== after.dev ||
|
|
337
|
+
before.ino !== after.ino ||
|
|
338
|
+
before.size !== after.size ||
|
|
339
|
+
before.mtimeNs !== after.mtimeNs ||
|
|
340
|
+
before.ctimeNs !== after.ctimeNs
|
|
341
|
+
)
|
|
342
|
+
return null;
|
|
343
|
+
return { identity: { dev: before.dev, ino: before.ino }, size, prefix, tail };
|
|
344
|
+
} catch {
|
|
345
|
+
return null;
|
|
346
|
+
} finally {
|
|
347
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function readExactly(fd: number, position: number, length: number): Buffer {
|
|
352
|
+
const buffer = Buffer.alloc(length);
|
|
353
|
+
let offset = 0;
|
|
354
|
+
while (offset < length) {
|
|
355
|
+
const bytesRead = fs.readSync(fd, buffer, offset, length - offset, position + offset);
|
|
356
|
+
if (bytesRead === 0) throw new Error("Short session file read");
|
|
357
|
+
offset += bytesRead;
|
|
358
|
+
}
|
|
359
|
+
return buffer;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function sameIdentity(left: FileIdentity, right: FileIdentity): boolean {
|
|
363
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function digest(bytes: Buffer): string {
|
|
367
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function completePrefix(bytes: Buffer): { bytes: Buffer } | undefined {
|
|
371
|
+
const newline = bytes.lastIndexOf(0x0a);
|
|
372
|
+
return newline < 0 ? undefined : { bytes: bytes.subarray(0, newline + 1) };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function parseCompleteEntries(bytes: Buffer): { entries: SessionMessageEntry[]; model?: string } | null {
|
|
376
|
+
try {
|
|
377
|
+
const text = FATAL_UTF8_DECODER.decode(bytes);
|
|
378
|
+
for (const line of text.split("\n")) if (line.trim()) JSON.parse(line);
|
|
379
|
+
const parsed = parseSessionEntries(text);
|
|
380
|
+
return {
|
|
381
|
+
entries: parsed.filter((entry): entry is SessionMessageEntry => entry.type === "message"),
|
|
382
|
+
model: modelFromEntries(parsed),
|
|
383
|
+
};
|
|
274
384
|
} catch {
|
|
275
385
|
return null;
|
|
276
386
|
}
|
|
277
387
|
}
|
|
388
|
+
|
|
389
|
+
function modelFromEntries(entries: readonly FileEntry[]): string | undefined {
|
|
390
|
+
let model: string | undefined;
|
|
391
|
+
for (const entry of entries) {
|
|
392
|
+
if (entry.type === "model_change") model = entry.model;
|
|
393
|
+
else if (entry.type === "message" && entry.message.role === "assistant" && entry.message.model)
|
|
394
|
+
model = entry.message.model;
|
|
395
|
+
}
|
|
396
|
+
return model;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function isValidUtf8Prefix(bytes: Buffer): boolean {
|
|
400
|
+
try {
|
|
401
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: true });
|
|
402
|
+
return true;
|
|
403
|
+
} catch {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function cacheEntries(filePath: string, snapshot: StableSnapshot): ObserverCache | null {
|
|
409
|
+
const complete = completePrefix(snapshot.tail);
|
|
410
|
+
if (!complete) {
|
|
411
|
+
if (!isValidUtf8Prefix(snapshot.tail)) return null;
|
|
412
|
+
return {
|
|
413
|
+
path: filePath,
|
|
414
|
+
identity: snapshot.identity,
|
|
415
|
+
completeOffset: 0,
|
|
416
|
+
prefixDigest: digest(Buffer.alloc(0)),
|
|
417
|
+
entries: [],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const remainder = snapshot.tail.subarray(complete.bytes.length);
|
|
421
|
+
if (!isValidUtf8Prefix(remainder)) return null;
|
|
422
|
+
const parsed = parseCompleteEntries(complete.bytes);
|
|
423
|
+
if (!parsed) return null;
|
|
424
|
+
return {
|
|
425
|
+
path: filePath,
|
|
426
|
+
identity: snapshot.identity,
|
|
427
|
+
completeOffset: complete.bytes.length,
|
|
428
|
+
prefixDigest: digest(complete.bytes),
|
|
429
|
+
entries: parsed.entries,
|
|
430
|
+
model: parsed.model,
|
|
431
|
+
};
|
|
432
|
+
}
|