@gajae-code/coding-agent 0.11.11 → 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 +49 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- 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-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 +29 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +22 -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/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -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 +8 -0
- 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/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/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- 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-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +15 -1
- 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/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -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/session/agent-session.d.ts +44 -10
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +25 -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/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 +4 -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 +2 -0
- 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/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- 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/team.ts +1 -1
- 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 +23 -1
- package/src/config/settings.ts +246 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/types.ts +28 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- 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 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +742 -85
- package/src/gjc-runtime/state-runtime.ts +2 -1
- 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 +8 -9
- 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 +43 -7
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- 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 +61 -4
- package/src/modes/components/oauth-selector.ts +42 -12
- 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/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +19 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +19 -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/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +341 -38
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +13 -1
- package/src/session/agent-session.ts +466 -50
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +764 -17
- 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/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 +182 -18
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -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
|
@@ -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
|
+
}
|
|
@@ -235,18 +235,16 @@ function getSavedUsageMode(): UsageMode {
|
|
|
235
235
|
return segmentOptions.usage?.mode === "remaining" ? "remaining" : "used";
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
function
|
|
238
|
+
function getUsageModeSettings(mode: string): StatusLineSegmentOptions {
|
|
239
239
|
const normalizedMode: UsageMode = mode === "remaining" ? "remaining" : "used";
|
|
240
240
|
const segmentOptions = settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions;
|
|
241
|
-
|
|
241
|
+
return {
|
|
242
242
|
...segmentOptions,
|
|
243
243
|
usage: {
|
|
244
244
|
...(segmentOptions.usage ?? {}),
|
|
245
245
|
mode: normalizedMode,
|
|
246
246
|
},
|
|
247
247
|
};
|
|
248
|
-
settings.set("statusLine.segmentOptions", nextOptions as Record<string, unknown>);
|
|
249
|
-
return nextOptions;
|
|
250
248
|
}
|
|
251
249
|
|
|
252
250
|
function statusSegmentLabel(id: StatusLineSegmentId): string {
|
|
@@ -613,14 +611,17 @@ class StatusLineCustomEditor extends Container {
|
|
|
613
611
|
}
|
|
614
612
|
|
|
615
613
|
#save(): void {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
614
|
+
const saved = commitInteractiveSettings(this.callbacks, () => {
|
|
615
|
+
settings.set("statusLine.preset", "custom");
|
|
616
|
+
settings.set("statusLine.leftSegments", [...this.#draft.leftSegments]);
|
|
617
|
+
settings.set("statusLine.rightSegments", [...this.#draft.rightSegments]);
|
|
618
|
+
settings.set("statusLine.separator", this.#draft.separator);
|
|
619
|
+
settings.set(
|
|
620
|
+
"statusLine.segmentOptions",
|
|
621
|
+
cloneSegmentOptions(this.#draft.segmentOptions) as Record<string, unknown>,
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
if (!saved) return;
|
|
624
625
|
this.callbacks.onChange("statusLine.preset", "custom");
|
|
625
626
|
this.callbacks.onChange("statusLine.leftSegments", [...this.#draft.leftSegments]);
|
|
626
627
|
this.callbacks.onChange("statusLine.rightSegments", [...this.#draft.rightSegments]);
|
|
@@ -707,9 +708,29 @@ export interface SettingsCallbacks {
|
|
|
707
708
|
getStatusLinePreview?: (width?: number) => string;
|
|
708
709
|
/** Called when plugins change */
|
|
709
710
|
onPluginsChanged?: () => void;
|
|
711
|
+
/** Called when an interactive setting cannot be committed. */
|
|
712
|
+
onError?: (message: string) => void;
|
|
710
713
|
/** Called when settings panel is closed */
|
|
711
714
|
onCancel: () => void;
|
|
712
715
|
}
|
|
716
|
+
function commitInteractiveSettings(callbacks: SettingsCallbacks, commit: () => void): boolean {
|
|
717
|
+
if (!settings.canWriteDurableConfig()) {
|
|
718
|
+
callbacks.onError?.(
|
|
719
|
+
"Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
|
|
720
|
+
);
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
commit();
|
|
725
|
+
return true;
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if (!settings.canWriteDurableConfig()) {
|
|
728
|
+
callbacks.onError?.(error instanceof Error ? error.message : String(error));
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
713
734
|
|
|
714
735
|
/**
|
|
715
736
|
* Main tabbed settings selector component.
|
|
@@ -994,11 +1015,18 @@ export class SettingsSelectorComponent extends Container {
|
|
|
994
1015
|
// The shared pet commit policy rechecks capability immediately
|
|
995
1016
|
// before mutation and persists only on acceptance; the settings
|
|
996
1017
|
// surface must not persist ahead of that result.
|
|
997
|
-
|
|
1018
|
+
let accepted = false;
|
|
1019
|
+
if (
|
|
1020
|
+
!commitInteractiveSettings(this.callbacks, () => {
|
|
1021
|
+
accepted = this.callbacks.onPetCommit?.(value) ?? false;
|
|
1022
|
+
})
|
|
1023
|
+
) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
998
1026
|
done(accepted ? value : undefined);
|
|
999
1027
|
return;
|
|
1000
1028
|
}
|
|
1001
|
-
this.#setSettingValue(def.path, value);
|
|
1029
|
+
if (!commitInteractiveSettings(this.callbacks, () => this.#setSettingValue(def.path, value))) return;
|
|
1002
1030
|
this.callbacks.onChange(def.path, value);
|
|
1003
1031
|
done(value);
|
|
1004
1032
|
},
|
|
@@ -1031,7 +1059,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1031
1059
|
value => {
|
|
1032
1060
|
// Empty string clears the setting; undefined-typed string settings
|
|
1033
1061
|
// store "" which the browser.ts expandPath ignores (no-op fallback).
|
|
1034
|
-
this.#setSettingValue(def.path, value);
|
|
1062
|
+
if (!commitInteractiveSettings(this.callbacks, () => this.#setSettingValue(def.path, value))) return;
|
|
1035
1063
|
this.callbacks.onChange(def.path, value);
|
|
1036
1064
|
wrappedDone(value);
|
|
1037
1065
|
},
|
|
@@ -1089,7 +1117,15 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1089
1117
|
getSettingsListTheme(),
|
|
1090
1118
|
(id, newValue) => {
|
|
1091
1119
|
if (id === STATUS_LINE_USAGE_MODE_ID) {
|
|
1092
|
-
const segmentOptions =
|
|
1120
|
+
const segmentOptions = getUsageModeSettings(newValue);
|
|
1121
|
+
if (
|
|
1122
|
+
!commitInteractiveSettings(this.callbacks, () => {
|
|
1123
|
+
settings.set("statusLine.segmentOptions", segmentOptions as Record<string, unknown>);
|
|
1124
|
+
})
|
|
1125
|
+
) {
|
|
1126
|
+
this.#refreshCurrentTabItems(defs);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1093
1129
|
this.callbacks.onChange("statusLine.segmentOptions", segmentOptions);
|
|
1094
1130
|
if (tabId === "appearance") {
|
|
1095
1131
|
this.#triggerStatusLinePreview();
|
|
@@ -1105,14 +1141,20 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1105
1141
|
|
|
1106
1142
|
if (def.type === "boolean") {
|
|
1107
1143
|
const boolValue = newValue === "true";
|
|
1108
|
-
settings.set(path, boolValue as never)
|
|
1144
|
+
if (!commitInteractiveSettings(this.callbacks, () => settings.set(path, boolValue as never))) {
|
|
1145
|
+
this.#refreshCurrentTabItems(defs);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1109
1148
|
this.callbacks.onChange(path, boolValue);
|
|
1110
1149
|
|
|
1111
1150
|
if (tabId === "appearance") {
|
|
1112
1151
|
this.#triggerStatusLinePreview();
|
|
1113
1152
|
}
|
|
1114
1153
|
} else if (def.type === "enum") {
|
|
1115
|
-
settings.set(path, newValue as never)
|
|
1154
|
+
if (!commitInteractiveSettings(this.callbacks, () => settings.set(path, newValue as never))) {
|
|
1155
|
+
this.#refreshCurrentTabItems(defs);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1116
1158
|
this.callbacks.onChange(path, newValue);
|
|
1117
1159
|
}
|
|
1118
1160
|
// Submenu/text types already persisted the value inside their own
|