@gajae-code/coding-agent 0.8.2 → 0.9.1
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 +53 -0
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +119 -1
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/components/thinking-selector.d.ts +2 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +22 -0
- package/dist/types/notifications/index.d.ts +2 -0
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/telegram-daemon.d.ts +30 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +14 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +43 -1
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/package.json +8 -11
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/config-cli.ts +27 -3
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +11 -9
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +26 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/config/keybindings.ts +1 -1
- package/src/config/settings-schema.ts +76 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
- package/src/defaults/gjc/skills/team/SKILL.md +16 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/gjc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +181 -219
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-state.ts +31 -2
- package/src/gjc-runtime/launch-tmux.ts +6 -0
- package/src/gjc-runtime/session-state-sidecar.ts +208 -13
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +175 -19
- package/src/gjc-runtime/tmux-common.ts +1 -0
- package/src/gjc-runtime/ultragoal-guard.ts +55 -66
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1602 -143
- package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
- package/src/gjc-runtime/workflow-manifest.ts +74 -1
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +95 -4
- package/src/internal-urls/docs-index.generated.ts +18 -9
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +43 -2
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/thinking-selector.ts +10 -8
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/controllers/command-controller.ts +39 -2
- package/src/modes/controllers/event-controller.ts +77 -49
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +13 -18
- package/src/modes/controllers/selector-controller.ts +41 -0
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/rpc/rpc-mode.ts +32 -12
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +17 -4
- package/src/notifications/config-commands.ts +20 -0
- package/src/notifications/config.ts +43 -4
- package/src/notifications/index.ts +105 -9
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +142 -0
- package/src/notifications/telegram-daemon-cli.ts +54 -7
- package/src/notifications/telegram-daemon-control.ts +2 -2
- package/src/notifications/telegram-daemon.ts +390 -56
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +45 -2
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +26 -19
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +6 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/sdk.ts +68 -30
- package/src/session/agent-session.ts +734 -107
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +59 -2
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +143 -1
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +125 -20
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +4 -3
- package/src/task/output-manager.ts +4 -2
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/utils/pasted-image-path.ts +170 -0
- package/src/web/scrapers/youtube.ts +24 -12
package/src/lsp/client.ts
CHANGED
|
@@ -835,6 +835,7 @@ async function shutdownClientInstance(client: LspClient): Promise<void> {
|
|
|
835
835
|
await Promise.race([shutdown, Bun.sleep(5_000)]);
|
|
836
836
|
await client.owner?.dispose();
|
|
837
837
|
await client.owner?.awaitExit({ timeoutMs: 1_000 });
|
|
838
|
+
await Promise.race([client.proc.exited.catch(() => undefined), Bun.sleep(1_000)]);
|
|
838
839
|
}
|
|
839
840
|
|
|
840
841
|
export async function shutdownClient(key: string): Promise<void> {
|
package/src/main.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { runStartupCredentialAutoImportIfNeeded } from "./setup/credential-auto-
|
|
|
52
52
|
import { formatModelOnboardingGuidance } from "./setup/model-onboarding-guidance";
|
|
53
53
|
import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
|
|
54
54
|
import { resolvePromptInput } from "./system-prompt";
|
|
55
|
+
import { persistTaskTokenLog, resolveTaskTokenLogDir, taskTokenLogFromUsage } from "./task/token-log";
|
|
55
56
|
import type { LspStartupServerInfo } from "./tools";
|
|
56
57
|
import { getDisplayChangelogEntries, getInstalledVersionChangelogEntry, getNewEntries } from "./utils/changelog";
|
|
57
58
|
import type { EventBus } from "./utils/event-bus";
|
|
@@ -513,8 +514,8 @@ export async function createSessionManager(
|
|
|
513
514
|
}
|
|
514
515
|
return manager;
|
|
515
516
|
}
|
|
516
|
-
|
|
517
|
-
return
|
|
517
|
+
const sessionDir = parsed.sessionDir ?? SessionManager.getDefaultSessionDir(cwd, activeSettings.getAgentDir());
|
|
518
|
+
return SessionManager.create(cwd, sessionDir);
|
|
518
519
|
}
|
|
519
520
|
|
|
520
521
|
async function maybeAutoChdir(parsed: Args): Promise<void> {
|
|
@@ -951,6 +952,46 @@ export async function runRootCommand(
|
|
|
951
952
|
modelRegistry,
|
|
952
953
|
settingsInstance,
|
|
953
954
|
);
|
|
955
|
+
// Resolve the token-log dir lazily on the first chat-usage event: a fresh
|
|
956
|
+
// launch's session dir does not exist yet at this point (the SDK creates it),
|
|
957
|
+
// so eager resolution would miss it. Re-resolve when the SessionManager id
|
|
958
|
+
// changes mid-run (fork/new) so root turns keep landing in the SAME
|
|
959
|
+
// `<session>/token-logs` dir the task executor uses for subagent turns.
|
|
960
|
+
let rootTokenLogDir: string | undefined;
|
|
961
|
+
let rootTokenLogSessionId: string | undefined;
|
|
962
|
+
let rootTokenLogResolved = false;
|
|
963
|
+
let rootTokenTurn = 0;
|
|
964
|
+
const baseTelemetry = sessionOptions.telemetry;
|
|
965
|
+
sessionOptions.telemetry = {
|
|
966
|
+
...(baseTelemetry ?? {}),
|
|
967
|
+
onChatUsage: async event => {
|
|
968
|
+
await baseTelemetry?.onChatUsage?.(event);
|
|
969
|
+
const currentSessionId = sessionManager?.getSessionId();
|
|
970
|
+
if (!rootTokenLogResolved || (currentSessionId && currentSessionId !== rootTokenLogSessionId)) {
|
|
971
|
+
rootTokenLogDir = await resolveTaskTokenLogDir(process.cwd(), sessionManager);
|
|
972
|
+
// Reset the per-session turn counter when the log dir moves to a new
|
|
973
|
+
// session so each session's log starts at turn 1.
|
|
974
|
+
if (rootTokenLogSessionId !== undefined && currentSessionId !== rootTokenLogSessionId) rootTokenTurn = 0;
|
|
975
|
+
rootTokenLogSessionId = currentSessionId;
|
|
976
|
+
rootTokenLogResolved = true;
|
|
977
|
+
}
|
|
978
|
+
if (!rootTokenLogDir) return;
|
|
979
|
+
rootTokenTurn += 1;
|
|
980
|
+
await persistTaskTokenLog(
|
|
981
|
+
taskTokenLogFromUsage(event.usage, {
|
|
982
|
+
subagentId: "root",
|
|
983
|
+
agent: event.agent?.name ?? "main",
|
|
984
|
+
// Monotonic 1-based sequence of persisted usage events for this
|
|
985
|
+
// session (event.stepNumber is 0-based and -1 for oneshot spans).
|
|
986
|
+
turn: rootTokenTurn,
|
|
987
|
+
at: new Date().toISOString(),
|
|
988
|
+
model: event.model,
|
|
989
|
+
cost: event.cost,
|
|
990
|
+
}),
|
|
991
|
+
{ dir: rootTokenLogDir },
|
|
992
|
+
);
|
|
993
|
+
},
|
|
994
|
+
};
|
|
954
995
|
sessionOptions.authStorage = authStorage;
|
|
955
996
|
sessionOptions.modelRegistry = modelRegistry;
|
|
956
997
|
sessionOptions.hasUI = isInteractive || mode === "rpc-ui";
|
package/src/memories/index.ts
CHANGED
|
@@ -1083,7 +1083,18 @@ async function resolveMemoryModel(options: {
|
|
|
1083
1083
|
});
|
|
1084
1084
|
if (resolved.model) return resolved.model;
|
|
1085
1085
|
}
|
|
1086
|
-
|
|
1086
|
+
if (session.model) return session.model;
|
|
1087
|
+
// No configured role and no active session model (e.g. background startup
|
|
1088
|
+
// before lazy model resolution): prefer the most recently used model over
|
|
1089
|
+
// raw registry order. `getAll()[0]` can be a retired model the provider
|
|
1090
|
+
// now rejects with 404, which permanently fails consolidation jobs.
|
|
1091
|
+
const allModels = modelRegistry.getAll();
|
|
1092
|
+
const usageOrder = session.settings.getStorage()?.getModelUsageOrder() ?? [];
|
|
1093
|
+
for (const usedKey of usageOrder) {
|
|
1094
|
+
const match = allModels.find(model => `${model.provider}/${model.id}` === usedKey);
|
|
1095
|
+
if (match) return match;
|
|
1096
|
+
}
|
|
1097
|
+
return allModels[0];
|
|
1087
1098
|
}
|
|
1088
1099
|
|
|
1089
1100
|
function loadMemoryConfig(settings: Settings): MemoryRuntimeConfig {
|
|
@@ -55,6 +55,15 @@ function elideRunawayThinkingRepetition(text: string): string {
|
|
|
55
55
|
return text;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
interface AssistantMessageUpdateOptions {
|
|
59
|
+
streaming?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type AssistantChildDescriptor = {
|
|
63
|
+
key: string;
|
|
64
|
+
component: Component;
|
|
65
|
+
};
|
|
66
|
+
|
|
58
67
|
/**
|
|
59
68
|
* Component that renders a complete assistant message
|
|
60
69
|
*/
|
|
@@ -67,6 +76,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
67
76
|
#kittyConversionsInFlight = new Set<string>();
|
|
68
77
|
#responseHeader = new Text(theme.bold(theme.fg("statusLineModel", "gajae")), 1, 0);
|
|
69
78
|
#contentBlocksCache = new WeakMap<object, { source: string; component: Component }>();
|
|
79
|
+
#lastStreaming = false;
|
|
80
|
+
#childComponents = new Map<string, Component>();
|
|
81
|
+
#contentBlockKeys = new WeakMap<object, string>();
|
|
82
|
+
#nextContentBlockKey = 0;
|
|
83
|
+
#reusableChildren = new WeakSet<Component>();
|
|
70
84
|
|
|
71
85
|
constructor(
|
|
72
86
|
message?: AssistantMessage,
|
|
@@ -87,7 +101,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
87
101
|
override invalidate(): void {
|
|
88
102
|
super.invalidate();
|
|
89
103
|
if (this.#lastMessage) {
|
|
90
|
-
this.updateContent(this.#lastMessage);
|
|
104
|
+
this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
|
|
@@ -115,7 +129,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
115
129
|
this.#convertToolImagesForKitty(toolCallId, validImages);
|
|
116
130
|
}
|
|
117
131
|
if (this.#lastMessage) {
|
|
118
|
-
this.updateContent(this.#lastMessage);
|
|
132
|
+
this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
|
|
119
133
|
}
|
|
120
134
|
}
|
|
121
135
|
|
|
@@ -138,7 +152,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
138
152
|
mimeType: "image/png",
|
|
139
153
|
});
|
|
140
154
|
if (this.#lastMessage) {
|
|
141
|
-
this.updateContent(this.#lastMessage);
|
|
155
|
+
this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
|
|
142
156
|
}
|
|
143
157
|
this.onImageUpdate?.();
|
|
144
158
|
})
|
|
@@ -151,153 +165,298 @@ export class AssistantMessageComponent extends Container {
|
|
|
151
165
|
setUsageInfo(usage: Usage): void {
|
|
152
166
|
this.#usageInfo = usage;
|
|
153
167
|
if (this.#lastMessage) {
|
|
154
|
-
this.updateContent(this.#lastMessage);
|
|
168
|
+
this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#contentBlockKey(content: object): string {
|
|
173
|
+
let key = this.#contentBlockKeys.get(content);
|
|
174
|
+
if (!key) {
|
|
175
|
+
key = `block:${this.#nextContentBlockKey++}`;
|
|
176
|
+
this.#contentBlockKeys.set(content, key);
|
|
155
177
|
}
|
|
178
|
+
return key;
|
|
156
179
|
}
|
|
157
180
|
|
|
158
|
-
#
|
|
181
|
+
#cachedChild<T extends Component>(key: string, create: () => T): T {
|
|
182
|
+
const cached = this.#childComponents.get(key);
|
|
183
|
+
if (cached) return cached as T;
|
|
184
|
+
const component = create();
|
|
185
|
+
this.#childComponents.set(key, component);
|
|
186
|
+
this.#reusableChildren.add(component);
|
|
187
|
+
return component;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#renderTextBlock(content: { text: string }, streaming: boolean): Component {
|
|
159
191
|
const cached = this.#contentBlocksCache.get(content);
|
|
160
|
-
if (cached?.source === content.text)
|
|
192
|
+
if (cached?.source === content.text) {
|
|
193
|
+
if (cached.component instanceof Markdown) {
|
|
194
|
+
cached.component.setOnStaleThrottle(this.onImageUpdate);
|
|
195
|
+
cached.component.setStreaming(streaming);
|
|
196
|
+
}
|
|
197
|
+
return cached.component;
|
|
198
|
+
}
|
|
161
199
|
const trimmed = content.text.trim();
|
|
162
200
|
const deepInterview = renderDeepInterviewAssistantText(trimmed, theme);
|
|
163
201
|
// Reuse the same Markdown instance across streaming chunks (update text in place)
|
|
164
202
|
// instead of constructing a new one each chunk; combined with the markdown
|
|
165
203
|
// per-code-block highlight cache, appends no longer re-highlight the whole prefix.
|
|
166
204
|
if (!deepInterview && cached && cached.component instanceof Markdown) {
|
|
167
|
-
cached.component.
|
|
205
|
+
cached.component.setOnStaleThrottle(this.onImageUpdate);
|
|
206
|
+
cached.component.setText(trimmed, { streaming });
|
|
168
207
|
cached.source = content.text;
|
|
169
208
|
return cached.component;
|
|
170
209
|
}
|
|
171
210
|
const component = deepInterview ?? new Markdown(trimmed, 1, 0, getMarkdownTheme());
|
|
211
|
+
if (component instanceof Markdown) {
|
|
212
|
+
component.setOnStaleThrottle(this.onImageUpdate);
|
|
213
|
+
component.setStreaming(streaming);
|
|
214
|
+
}
|
|
172
215
|
this.#contentBlocksCache.set(content, { source: content.text, component });
|
|
216
|
+
this.#reusableChildren.add(component);
|
|
173
217
|
return component;
|
|
174
218
|
}
|
|
175
219
|
|
|
176
|
-
#renderThinkingBlock(content: { thinking: string }): Markdown {
|
|
220
|
+
#renderThinkingBlock(content: { thinking: string }, streaming: boolean): Markdown {
|
|
177
221
|
const cached = this.#contentBlocksCache.get(content);
|
|
178
|
-
if (cached?.source === content.thinking)
|
|
222
|
+
if (cached?.source === content.thinking) {
|
|
223
|
+
if (cached.component instanceof Markdown) {
|
|
224
|
+
cached.component.setOnStaleThrottle(this.onImageUpdate);
|
|
225
|
+
cached.component.setStreaming(streaming);
|
|
226
|
+
}
|
|
227
|
+
return cached.component as Markdown;
|
|
228
|
+
}
|
|
179
229
|
const trimmed = elideRunawayThinkingRepetition(content.thinking.trim());
|
|
230
|
+
if (cached?.component instanceof Markdown) {
|
|
231
|
+
cached.component.setOnStaleThrottle(this.onImageUpdate);
|
|
232
|
+
cached.component.setText(trimmed, { streaming });
|
|
233
|
+
cached.source = content.thinking;
|
|
234
|
+
return cached.component;
|
|
235
|
+
}
|
|
180
236
|
const component = new Markdown(trimmed, 1, 0, getMarkdownTheme(), {
|
|
181
237
|
color: (text: string) => theme.fg("thinkingText", text),
|
|
182
238
|
italic: true,
|
|
183
239
|
});
|
|
240
|
+
component.setOnStaleThrottle(this.onImageUpdate);
|
|
241
|
+
component.setStreaming(streaming);
|
|
184
242
|
this.#contentBlocksCache.set(content, { source: content.thinking, component });
|
|
243
|
+
this.#reusableChildren.add(component);
|
|
185
244
|
return component;
|
|
186
245
|
}
|
|
187
246
|
|
|
188
|
-
#
|
|
247
|
+
#toolImageDescriptors(): AssistantChildDescriptor[] {
|
|
189
248
|
const imageEntries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
|
|
190
249
|
images.map((image, index) => ({ image, key: `${toolCallId}:${index}` })),
|
|
191
250
|
);
|
|
192
|
-
if (imageEntries.length === 0) return;
|
|
251
|
+
if (imageEntries.length === 0) return [];
|
|
193
252
|
|
|
194
|
-
|
|
253
|
+
const descriptors: AssistantChildDescriptor[] = [
|
|
254
|
+
{ key: "tool-images:spacer", component: this.#cachedChild("tool-images:spacer", () => new Spacer(1)) },
|
|
255
|
+
];
|
|
195
256
|
for (const { image, key } of imageEntries) {
|
|
196
257
|
const displayImage =
|
|
197
258
|
TERMINAL.imageProtocol === ImageProtocol.Kitty && image.mimeType !== "image/png"
|
|
198
259
|
? this.#convertedKittyImages.get(key)
|
|
199
260
|
: image;
|
|
200
261
|
if (TERMINAL.imageProtocol && displayImage) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
262
|
+
const imageKey = `tool-image:${key}:${displayImage.mimeType}:${displayImage.data}`;
|
|
263
|
+
descriptors.push({
|
|
264
|
+
key: imageKey,
|
|
265
|
+
component: this.#cachedChild(
|
|
266
|
+
imageKey,
|
|
267
|
+
() =>
|
|
268
|
+
new Image(
|
|
269
|
+
displayImage.data,
|
|
270
|
+
displayImage.mimeType,
|
|
271
|
+
{ fallbackColor: (text: string) => theme.fg("toolOutput", text) },
|
|
272
|
+
resolveImageOptions(),
|
|
273
|
+
),
|
|
207
274
|
),
|
|
208
|
-
);
|
|
275
|
+
});
|
|
209
276
|
continue;
|
|
210
277
|
}
|
|
211
|
-
|
|
278
|
+
const fallbackKey = `tool-image-fallback:${key}:${image.mimeType}`;
|
|
279
|
+
descriptors.push({
|
|
280
|
+
key: fallbackKey,
|
|
281
|
+
component: this.#cachedChild(
|
|
282
|
+
fallbackKey,
|
|
283
|
+
() => new Text(theme.fg("toolOutput", `[Image: ${image.mimeType}]`), 1, 0),
|
|
284
|
+
),
|
|
285
|
+
});
|
|
212
286
|
}
|
|
287
|
+
return descriptors;
|
|
213
288
|
}
|
|
214
289
|
|
|
215
|
-
|
|
216
|
-
|
|
290
|
+
#reconcileChildren(descriptors: AssistantChildDescriptor[]): void {
|
|
291
|
+
const nextChildren = descriptors.map(descriptor => descriptor.component);
|
|
292
|
+
if (
|
|
293
|
+
nextChildren.length === this.#contentContainer.children.length &&
|
|
294
|
+
nextChildren.every((child, index) => child === this.#contentContainer.children[index])
|
|
295
|
+
) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
217
298
|
|
|
218
|
-
|
|
219
|
-
this.#contentContainer.
|
|
299
|
+
const nextSet = new Set(nextChildren);
|
|
300
|
+
for (const child of this.#contentContainer.children) {
|
|
301
|
+
if (!nextSet.has(child) && !this.#reusableChildren.has(child)) {
|
|
302
|
+
child.dispose?.();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
this.#contentContainer.children = nextChildren;
|
|
306
|
+
}
|
|
220
307
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
308
|
+
updateContent(message: AssistantMessage, options?: AssistantMessageUpdateOptions): void {
|
|
309
|
+
this.#lastMessage = message;
|
|
310
|
+
this.#lastStreaming = options?.streaming ?? false;
|
|
224
311
|
|
|
312
|
+
const visibleContentAfter = new Array<boolean>(message.content.length).fill(false);
|
|
313
|
+
let hasVisibleContentAfter = false;
|
|
314
|
+
let hasVisibleContent = false;
|
|
315
|
+
let activeContentIndex = -1;
|
|
316
|
+
for (let i = message.content.length - 1; i >= 0; i--) {
|
|
317
|
+
visibleContentAfter[i] = hasVisibleContentAfter;
|
|
318
|
+
const content = message.content[i];
|
|
319
|
+
const isVisible =
|
|
320
|
+
(content.type === "text" && Boolean(content.text.trim())) ||
|
|
321
|
+
(content.type === "thinking" && Boolean(content.thinking.trim()));
|
|
322
|
+
if (isVisible) {
|
|
323
|
+
hasVisibleContent = true;
|
|
324
|
+
hasVisibleContentAfter = true;
|
|
325
|
+
if (this.#lastStreaming && activeContentIndex === -1) {
|
|
326
|
+
activeContentIndex = i;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const streaming = this.#lastStreaming;
|
|
332
|
+
const descriptors: AssistantChildDescriptor[] = [];
|
|
225
333
|
if (hasVisibleContent) {
|
|
226
|
-
|
|
227
|
-
|
|
334
|
+
descriptors.push({
|
|
335
|
+
key: "response-header:spacer",
|
|
336
|
+
component: this.#cachedChild("response-header:spacer", () => new Spacer(1)),
|
|
337
|
+
});
|
|
338
|
+
descriptors.push({ key: "response-header", component: this.#responseHeader });
|
|
228
339
|
}
|
|
229
340
|
|
|
230
341
|
// Render content in order
|
|
231
342
|
for (let i = 0; i < message.content.length; i++) {
|
|
232
343
|
const content = message.content[i];
|
|
344
|
+
const blockKey = this.#contentBlockKey(content);
|
|
233
345
|
if (content.type === "text" && content.text.trim()) {
|
|
234
|
-
|
|
346
|
+
descriptors.push({
|
|
347
|
+
key: `${blockKey}:text`,
|
|
348
|
+
component: this.#renderTextBlock(content, streaming && i === activeContentIndex),
|
|
349
|
+
});
|
|
235
350
|
} else if (content.type === "thinking" && content.thinking.trim()) {
|
|
236
351
|
// Add spacing only when another visible assistant content block follows.
|
|
237
352
|
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
238
|
-
const hasVisibleContentAfter = message.content
|
|
239
|
-
.slice(i + 1)
|
|
240
|
-
.some(c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
|
|
241
|
-
|
|
242
353
|
if (this.hideThinkingBlock) {
|
|
243
354
|
// Show static "Thinking..." label when hidden
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.#
|
|
247
|
-
|
|
355
|
+
descriptors.push({
|
|
356
|
+
key: `${blockKey}:thinking-hidden`,
|
|
357
|
+
component: this.#cachedChild(
|
|
358
|
+
`${blockKey}:thinking-hidden`,
|
|
359
|
+
() => new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 1, 0),
|
|
360
|
+
),
|
|
361
|
+
});
|
|
248
362
|
} else {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.#
|
|
252
|
-
}
|
|
363
|
+
descriptors.push({
|
|
364
|
+
key: `${blockKey}:thinking`,
|
|
365
|
+
component: this.#renderThinkingBlock(content, streaming && i === activeContentIndex),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if (visibleContentAfter[i]) {
|
|
369
|
+
descriptors.push({
|
|
370
|
+
key: `${blockKey}:thinking-spacer`,
|
|
371
|
+
component: this.#cachedChild(`${blockKey}:thinking-spacer`, () => new Spacer(1)),
|
|
372
|
+
});
|
|
253
373
|
}
|
|
254
374
|
}
|
|
255
375
|
}
|
|
256
376
|
|
|
257
|
-
this.#
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
377
|
+
descriptors.push(...this.#toolImageDescriptors());
|
|
378
|
+
|
|
379
|
+
const isTerminal = !streaming || Boolean(message.stopReason);
|
|
380
|
+
if (isTerminal) {
|
|
381
|
+
// Check if aborted - show after partial content
|
|
382
|
+
// But only if there are no tool calls (tool execution components will show the error)
|
|
383
|
+
const hasToolCalls = message.content.some(c => c.type === "toolCall");
|
|
384
|
+
if (!hasToolCalls) {
|
|
385
|
+
if (message.stopReason === "aborted" && !isSilentAbort(message.errorMessage)) {
|
|
386
|
+
const abortMessage =
|
|
387
|
+
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
388
|
+
? message.errorMessage
|
|
389
|
+
: "Operation aborted";
|
|
390
|
+
descriptors.push({
|
|
391
|
+
key: "abort:spacer",
|
|
392
|
+
component: this.#cachedChild("abort:spacer", () => new Spacer(1)),
|
|
393
|
+
});
|
|
394
|
+
descriptors.push({
|
|
395
|
+
key: `abort:text:${abortMessage}`,
|
|
396
|
+
component: this.#cachedChild(
|
|
397
|
+
`abort:text:${abortMessage}`,
|
|
398
|
+
() => new Text(theme.fg("error", abortMessage), 1, 0),
|
|
399
|
+
),
|
|
400
|
+
});
|
|
401
|
+
} else if (message.stopReason === "error") {
|
|
402
|
+
const errorMsg = message.errorMessage || "Unknown error";
|
|
403
|
+
descriptors.push({
|
|
404
|
+
key: "error:spacer",
|
|
405
|
+
component: this.#cachedChild("error:spacer", () => new Spacer(1)),
|
|
406
|
+
});
|
|
407
|
+
descriptors.push({
|
|
408
|
+
key: `error:text:${errorMsg}`,
|
|
409
|
+
component: this.#cachedChild(
|
|
410
|
+
`error:text:${errorMsg}`,
|
|
411
|
+
() => new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0),
|
|
412
|
+
),
|
|
413
|
+
});
|
|
271
414
|
}
|
|
272
|
-
this.#contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
|
|
273
|
-
} else if (message.stopReason === "error") {
|
|
274
|
-
const errorMsg = message.errorMessage || "Unknown error";
|
|
275
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
276
|
-
this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
277
415
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
416
|
+
if (
|
|
417
|
+
message.errorMessage &&
|
|
418
|
+
!isSilentAbort(message.errorMessage) &&
|
|
419
|
+
message.stopReason !== "aborted" &&
|
|
420
|
+
message.stopReason !== "error"
|
|
421
|
+
) {
|
|
422
|
+
descriptors.push({
|
|
423
|
+
key: "message-error:spacer",
|
|
424
|
+
component: this.#cachedChild("message-error:spacer", () => new Spacer(1)),
|
|
425
|
+
});
|
|
426
|
+
descriptors.push({
|
|
427
|
+
key: `message-error:text:${message.errorMessage}`,
|
|
428
|
+
component: this.#cachedChild(
|
|
429
|
+
`message-error:text:${message.errorMessage}`,
|
|
430
|
+
() => new Text(theme.fg("error", `Error: ${message.errorMessage}`), 1, 0),
|
|
431
|
+
),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
288
434
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
435
|
+
// Token usage metadata
|
|
436
|
+
if (settings.get("display.showTokenUsage") && this.#usageInfo) {
|
|
437
|
+
const usage = this.#usageInfo;
|
|
438
|
+
const totalInput = usage.input + usage.cacheWrite;
|
|
439
|
+
const parts: string[] = [];
|
|
440
|
+
parts.push(`${theme.icon.input} ${formatNumber(totalInput)}`);
|
|
441
|
+
parts.push(`${theme.icon.output} ${formatNumber(usage.output)}`);
|
|
442
|
+
if (usage.cacheRead > 0) {
|
|
443
|
+
parts.push(`cache: ${formatNumber(usage.cacheRead)}`);
|
|
444
|
+
}
|
|
445
|
+
const usageText = parts.join(" ");
|
|
446
|
+
descriptors.push({
|
|
447
|
+
key: "usage:spacer",
|
|
448
|
+
component: this.#cachedChild("usage:spacer", () => new Spacer(1)),
|
|
449
|
+
});
|
|
450
|
+
descriptors.push({
|
|
451
|
+
key: `usage:text:${usageText}`,
|
|
452
|
+
component: this.#cachedChild(
|
|
453
|
+
`usage:text:${usageText}`,
|
|
454
|
+
() => new Text(theme.fg("dim", usageText), 1, 0),
|
|
455
|
+
),
|
|
456
|
+
});
|
|
298
457
|
}
|
|
299
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
300
|
-
this.#contentContainer.addChild(new Text(theme.fg("dim", parts.join(" ")), 1, 0));
|
|
301
458
|
}
|
|
459
|
+
|
|
460
|
+
this.#reconcileChildren(descriptors);
|
|
302
461
|
}
|
|
303
462
|
}
|
|
@@ -16,6 +16,7 @@ import { resolveCurrentBranch } from "./status-line/git-utils";
|
|
|
16
16
|
export class FooterComponent implements Component {
|
|
17
17
|
#cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name
|
|
18
18
|
#gitWatcher: fs.FSWatcher | null = null;
|
|
19
|
+
#gitWatcherGeneration = 0;
|
|
19
20
|
#onBranchChange: (() => void) | null = null;
|
|
20
21
|
#autoCompactEnabled: boolean = true;
|
|
21
22
|
#extensionStatuses: Map<string, string> = new Map();
|
|
@@ -51,7 +52,10 @@ export class FooterComponent implements Component {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
#setupGitWatcher(): void {
|
|
54
|
-
// Clean up existing watcher
|
|
55
|
+
// Clean up existing watcher. The generation guard below keeps a stale
|
|
56
|
+
// in-flight resolve — one that dispose() or a newer setup already
|
|
57
|
+
// superseded — from installing a watcher nothing would ever close.
|
|
58
|
+
const generation = ++this.#gitWatcherGeneration;
|
|
55
59
|
if (this.#gitWatcher) {
|
|
56
60
|
this.#gitWatcher.close();
|
|
57
61
|
this.#gitWatcher = null;
|
|
@@ -60,7 +64,7 @@ export class FooterComponent implements Component {
|
|
|
60
64
|
void git.head
|
|
61
65
|
.resolve(getProjectDir())
|
|
62
66
|
.then(head => {
|
|
63
|
-
if (!head) {
|
|
67
|
+
if (!head || generation !== this.#gitWatcherGeneration) {
|
|
64
68
|
return;
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -84,6 +88,7 @@ export class FooterComponent implements Component {
|
|
|
84
88
|
* Clean up the file watcher
|
|
85
89
|
*/
|
|
86
90
|
dispose(): void {
|
|
91
|
+
this.#gitWatcherGeneration++;
|
|
87
92
|
if (this.#gitWatcher) {
|
|
88
93
|
this.#gitWatcher.close();
|
|
89
94
|
this.#gitWatcher = null;
|