@gajae-code/coding-agent 0.10.0 → 0.10.2
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 +33 -0
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/policy.d.ts +3 -0
- package/dist/types/coordinator-mcp/server.d.ts +17 -5
- package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
- package/dist/types/extensibility/extensions/types.d.ts +3 -1
- package/dist/types/modes/components/gajae-pet-widget.d.ts +1 -0
- package/dist/types/modes/components/pet-capability.d.ts +37 -0
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +0 -10
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/print-mode.d.ts +3 -4
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
- package/dist/types/modes/types.d.ts +11 -1
- package/dist/types/modes/utils/context-usage.d.ts +4 -19
- package/dist/types/session/agent-session.d.ts +14 -1
- package/dist/types/session/context-estimation.d.ts +31 -0
- package/package.json +7 -7
- package/src/cli.ts +1 -0
- package/src/config/model-profiles.ts +19 -19
- package/src/config/settings.ts +96 -45
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/policy.ts +30 -0
- package/src/coordinator-mcp/server.ts +294 -66
- package/src/coordinator-mcp/session-reaper.ts +129 -0
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/extensibility/slash-commands.ts +3 -3
- package/src/gjc-runtime/tmux-owner-isolation.ts +4 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +3 -5
- package/src/modes/acp/acp-agent.ts +2 -2
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +3 -3
- package/src/modes/components/gajae-pet-widget.ts +169 -20
- package/src/modes/components/pet-capability.ts +115 -0
- package/src/modes/components/pet-selector.ts +4 -6
- package/src/modes/components/settings-selector.ts +28 -2
- package/src/modes/components/status-line/segments.ts +13 -6
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +4 -171
- package/src/modes/controllers/extension-ui-controller.ts +10 -9
- package/src/modes/controllers/input-controller.ts +43 -13
- package/src/modes/controllers/selector-controller.ts +7 -18
- package/src/modes/interactive-mode.ts +65 -4
- package/src/modes/print-mode.ts +228 -92
- package/src/modes/rpc/rpc-client.ts +58 -1
- package/src/modes/rpc/rpc-mode.ts +286 -85
- package/src/modes/rpc/rpc-types.ts +15 -1
- package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
- package/src/modes/shared/agent-wire/command-validation.ts +10 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +11 -1
- package/src/modes/utils/context-usage.ts +56 -92
- package/src/session/agent-session.ts +337 -90
- package/src/session/context-estimation.ts +91 -0
- package/src/session/session-manager.ts +9 -2
- package/src/slash-commands/builtin-registry.ts +31 -19
- package/src/slash-commands/helpers/context-report.ts +26 -4
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
|
|
3
|
-
import { estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
|
|
2
|
+
|
|
4
3
|
import { type Component, truncateToWidth, visibleWidth } from "@gajae-code/tui";
|
|
5
4
|
import { formatCount, getProjectDir } from "@gajae-code/utils";
|
|
6
5
|
import { $ } from "bun";
|
|
@@ -13,7 +12,6 @@ import * as git from "../../utils/git";
|
|
|
13
12
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../../utils/session-color";
|
|
14
13
|
import { EMPTY_JOBS_SNAPSHOT, type JobsSnapshot } from "../jobs-observer";
|
|
15
14
|
import { sanitizeStatusText } from "../shared";
|
|
16
|
-
import { computeNonMessageTokens } from "../utils/context-usage";
|
|
17
15
|
import { renderSkillHudBar } from "./skill-hud/render";
|
|
18
16
|
import {
|
|
19
17
|
canReuseCachedPr,
|
|
@@ -70,104 +68,6 @@ interface CollectedStatusSegments {
|
|
|
70
68
|
rightCapWidth: number;
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
74
|
-
// Per-message token cache
|
|
75
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Symbol-keyed sidecar tagged onto each `AgentMessage` to memoize its
|
|
79
|
-
* `estimateMessageTokensHeuristic` result. Keyed by message identity (the object itself);
|
|
80
|
-
* a cheap content fingerprint detects in-place mutations (post-hoc error
|
|
81
|
-
* attachment, retry-truncated branch rebuild, etc.) and forces recompute.
|
|
82
|
-
*
|
|
83
|
-
* Cache lives on the message — multiple `StatusLineComponent` instances
|
|
84
|
-
* share it for free, and entries collect with the message itself when the
|
|
85
|
-
* conversation is replaced or compacted.
|
|
86
|
-
*/
|
|
87
|
-
const kTokenCache = Symbol("statusLine.tokenCache");
|
|
88
|
-
interface TaggedMessage {
|
|
89
|
-
[kTokenCache]?: { fingerprint: string; tokens: number };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Cheap structural fingerprint mirroring `estimateMessageTokensHeuristic`'s content walk.
|
|
94
|
-
* O(blocks) — only reads string `.length` and primitives, never copies or
|
|
95
|
-
* serializes content. Any in-place mutation that alters total tokenized
|
|
96
|
-
* content also alters one of the byte-length sums or block counts captured
|
|
97
|
-
* here, forcing the cached heuristic token value to be recomputed.
|
|
98
|
-
*/
|
|
99
|
-
function messageFingerprint(msg: AgentMessage): string {
|
|
100
|
-
const role = (msg as { role?: string }).role ?? "";
|
|
101
|
-
const ts = (msg as { timestamp?: number }).timestamp ?? 0;
|
|
102
|
-
let textLen = 0;
|
|
103
|
-
let blocks = 0;
|
|
104
|
-
let images = 0;
|
|
105
|
-
if (role === "bashExecution") {
|
|
106
|
-
const b = msg as { command?: unknown; output?: unknown };
|
|
107
|
-
if (typeof b.command === "string") textLen += b.command.length;
|
|
108
|
-
if (typeof b.output === "string") textLen += b.output.length;
|
|
109
|
-
} else if (role === "user") {
|
|
110
|
-
const content = (msg as { content?: unknown }).content;
|
|
111
|
-
if (typeof content === "string") {
|
|
112
|
-
textLen += content.length;
|
|
113
|
-
} else if (Array.isArray(content)) {
|
|
114
|
-
blocks = content.length;
|
|
115
|
-
for (const block of content) {
|
|
116
|
-
if (block?.type === "text" && typeof block.text === "string") textLen += block.text.length;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
} else if (role === "assistant") {
|
|
120
|
-
const content = (msg as { content?: unknown }).content;
|
|
121
|
-
if (Array.isArray(content)) {
|
|
122
|
-
blocks = content.length;
|
|
123
|
-
for (const block of content) {
|
|
124
|
-
if (!block || typeof block !== "object") continue;
|
|
125
|
-
const b = block as { type?: string; text?: string; thinking?: string; name?: string; arguments?: unknown };
|
|
126
|
-
if (b.type === "text" && typeof b.text === "string") textLen += b.text.length;
|
|
127
|
-
else if (b.type === "thinking" && typeof b.thinking === "string") textLen += b.thinking.length;
|
|
128
|
-
else if (b.type === "toolCall") {
|
|
129
|
-
if (typeof b.name === "string") textLen += b.name.length;
|
|
130
|
-
// Argument bytes vary; a length proxy is enough to detect in-place edits.
|
|
131
|
-
textLen += b.arguments === undefined ? 0 : JSON.stringify(b.arguments).length;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
} else if (role === "toolResult" || role === "hookMessage") {
|
|
136
|
-
const content = (msg as { content?: unknown }).content;
|
|
137
|
-
if (typeof content === "string") {
|
|
138
|
-
textLen += content.length;
|
|
139
|
-
} else if (Array.isArray(content)) {
|
|
140
|
-
blocks = content.length;
|
|
141
|
-
for (const block of content) {
|
|
142
|
-
if (!block || typeof block !== "object") continue;
|
|
143
|
-
const b = block as { type?: string; text?: string };
|
|
144
|
-
if (b.type === "text" && typeof b.text === "string") textLen += b.text.length;
|
|
145
|
-
else if (b.type === "image") images++;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
} else if (role === "branchSummary" || role === "compactionSummary") {
|
|
149
|
-
const s = (msg as { summary?: unknown }).summary;
|
|
150
|
-
if (typeof s === "string") textLen += s.length;
|
|
151
|
-
}
|
|
152
|
-
return `${role}:${ts}:${textLen}:${blocks}:${images}`;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Token count for a single message, using the per-message sidecar cache.
|
|
157
|
-
* The caller MUST skip caching for the last message during streaming —
|
|
158
|
-
* it may still be growing and its tokens belong recomputed each refresh.
|
|
159
|
-
*/
|
|
160
|
-
function tokensForMessage(msg: AgentMessage): number {
|
|
161
|
-
const fp = messageFingerprint(msg);
|
|
162
|
-
const tagged = msg as TaggedMessage;
|
|
163
|
-
const cached = tagged[kTokenCache];
|
|
164
|
-
if (cached && cached.fingerprint === fp) return cached.tokens;
|
|
165
|
-
const tokens = estimateMessageTokensHeuristic(msg);
|
|
166
|
-
tagged[kTokenCache] = { fingerprint: fp, tokens };
|
|
167
|
-
return tokens;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
171
71
|
// StatusLineComponent
|
|
172
72
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
173
73
|
|
|
@@ -217,20 +117,6 @@ export class StatusLineComponent implements Component {
|
|
|
217
117
|
#cachedUsage: SegmentContext["usage"] = null;
|
|
218
118
|
#usageFetchedAt = 0;
|
|
219
119
|
#usageInFlight = false;
|
|
220
|
-
// Context breakdown — incremental cache. Replaces the previous 2-second
|
|
221
|
-
// TTL design (which re-walked every message on each refresh and produced
|
|
222
|
-
// ~1.1 s sync freezes on 2,000+ message sessions because `updateEditorTopBorder`
|
|
223
|
-
// is called on every agent event in event-controller). The new scheme
|
|
224
|
-
// caches by message-object identity (a Symbol-keyed sidecar on each
|
|
225
|
-
// message) plus a cheap content fingerprint, so in-place mutations of
|
|
226
|
-
// an existing message (post-hoc error attachment, retry-truncated
|
|
227
|
-
// branch rebuild, replaceMessages with the same length) are detected
|
|
228
|
-
// and recomputed.
|
|
229
|
-
// Cached non-message total (system prompt + tools + skills). Invalidated
|
|
230
|
-
// when the inputs-identity fingerprint changes (model swap, skill toggle,
|
|
231
|
-
// tool registration).
|
|
232
|
-
#nonMessageTokensCache: number | undefined;
|
|
233
|
-
#nonMessageInputsKey: string | undefined;
|
|
234
120
|
|
|
235
121
|
constructor(
|
|
236
122
|
private readonly session: AgentSession,
|
|
@@ -618,57 +504,6 @@ export class StatusLineComponent implements Component {
|
|
|
618
504
|
return windows.length > 0 ? { windows } : null;
|
|
619
505
|
}
|
|
620
506
|
|
|
621
|
-
/**
|
|
622
|
-
* Compute the (cached) used-tokens / context-window totals for the
|
|
623
|
-
* status-line context% segment. Exposed (non-private) so unit tests can
|
|
624
|
-
* verify the incremental-cache invariants; not part of any external
|
|
625
|
-
* API.
|
|
626
|
-
*/
|
|
627
|
-
getCachedContextBreakdown(): { usedTokens: number; contextWindow: number } {
|
|
628
|
-
const messages = this.session.messages ?? [];
|
|
629
|
-
const contextWindow = this.session.model?.contextWindow ?? 0;
|
|
630
|
-
|
|
631
|
-
// 1) Non-message tokens (system prompt + tools + skills). Refresh only
|
|
632
|
-
// when the inputs identity fingerprint changes — usually never
|
|
633
|
-
// during a streaming turn. ~10-30 ms when it does refresh.
|
|
634
|
-
const inputsKey = this.#computeNonMessageInputsKey();
|
|
635
|
-
if (this.#nonMessageTokensCache === undefined || this.#nonMessageInputsKey !== inputsKey) {
|
|
636
|
-
this.#nonMessageTokensCache = computeNonMessageTokens(this.session);
|
|
637
|
-
this.#nonMessageInputsKey = inputsKey;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// 2) Message tokens — incremental. The sidecar cache lives on the
|
|
641
|
-
// message object itself (Symbol-keyed), keyed by identity and
|
|
642
|
-
// validated by a cheap content fingerprint. Mutations that
|
|
643
|
-
// replace messages (replaceMessages, branch rebuild, compaction)
|
|
644
|
-
// yield fresh objects → cache miss → recompute. In-place
|
|
645
|
-
// mutations on the same object are caught by fingerprint
|
|
646
|
-
// mismatch. The LAST message is always recomputed because it
|
|
647
|
-
// may still be growing during streaming.
|
|
648
|
-
let messagesTokens = 0;
|
|
649
|
-
const lastIdx = messages.length - 1;
|
|
650
|
-
for (let i = 0; i < messages.length; i++) {
|
|
651
|
-
messagesTokens += i === lastIdx ? estimateMessageTokensHeuristic(messages[i]) : tokensForMessage(messages[i]);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
const usedTokens = this.#nonMessageTokensCache + messagesTokens;
|
|
655
|
-
return { usedTokens, contextWindow };
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Build an identity fingerprint for the non-message inputs (system prompt,
|
|
660
|
-
* tools, skills). When this changes, the non-message token cache must be
|
|
661
|
-
* recomputed. Cheap: just lengths + first-string-length. Doesn't need to
|
|
662
|
-
* be cryptographically unique — only stable for the same inputs.
|
|
663
|
-
*/
|
|
664
|
-
#computeNonMessageInputsKey(): string {
|
|
665
|
-
const sp = this.session.systemPrompt ?? [];
|
|
666
|
-
const tools = this.session.agent?.state?.tools ?? [];
|
|
667
|
-
const skills = this.session.skills ?? [];
|
|
668
|
-
const modelId = this.session.model?.id ?? "";
|
|
669
|
-
return `${modelId}|${sp.length}:${sp[0]?.length ?? 0}|${tools.length}|${skills.length}`;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
507
|
#buildSegmentContext(
|
|
673
508
|
width: number,
|
|
674
509
|
effectiveSettings: Required<
|
|
@@ -695,11 +530,9 @@ export class StatusLineComponent implements Component {
|
|
|
695
530
|
tokensPerSecond: this.#getTokensPerSecond(),
|
|
696
531
|
};
|
|
697
532
|
|
|
698
|
-
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
const contextWindow = breakdown.contextWindow || state.model?.contextWindow || 0;
|
|
702
|
-
const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
533
|
+
const contextUsage = this.session.getContextUsage?.();
|
|
534
|
+
const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
535
|
+
const contextPercent = contextUsage?.percent ?? null;
|
|
703
536
|
// Suppress the inline model percentage when a standalone context_pct
|
|
704
537
|
// segment is also rendered, so the value is not shown twice.
|
|
705
538
|
const contextPctSegmentActive =
|
|
@@ -50,6 +50,11 @@ export class ExtensionUiController {
|
|
|
50
50
|
overlay?.hide();
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** Re-mount the pet-aware composer after a transient hook UI closes. */
|
|
54
|
+
#restoreComposerEditor(): void {
|
|
55
|
+
this.ctx.restoreComposer();
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
/**
|
|
54
59
|
* Initialize the hook system with TUI-based UI context.
|
|
55
60
|
*/
|
|
@@ -578,7 +583,7 @@ export class ExtensionUiController {
|
|
|
578
583
|
shutdown: () => {
|
|
579
584
|
// Signal shutdown request
|
|
580
585
|
},
|
|
581
|
-
getSystemPrompt: () => this.ctx.session.systemPrompt,
|
|
586
|
+
getSystemPrompt: () => [...this.ctx.session.systemPrompt],
|
|
582
587
|
});
|
|
583
588
|
} catch (err) {
|
|
584
589
|
this.showToolError(registeredTool.definition.name, err instanceof Error ? err.message : String(err));
|
|
@@ -723,8 +728,7 @@ export class ExtensionUiController {
|
|
|
723
728
|
*/
|
|
724
729
|
hideHookSelector(): void {
|
|
725
730
|
this.ctx.hookSelector?.dispose();
|
|
726
|
-
this
|
|
727
|
-
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
731
|
+
this.#restoreComposerEditor();
|
|
728
732
|
this.ctx.hookSelector = undefined;
|
|
729
733
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
730
734
|
this.ctx.ui.requestRender();
|
|
@@ -785,8 +789,7 @@ export class ExtensionUiController {
|
|
|
785
789
|
*/
|
|
786
790
|
hideHookInput(): void {
|
|
787
791
|
this.ctx.hookInput?.dispose();
|
|
788
|
-
this
|
|
789
|
-
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
792
|
+
this.#restoreComposerEditor();
|
|
790
793
|
this.ctx.hookInput = undefined;
|
|
791
794
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
792
795
|
this.ctx.ui.requestRender();
|
|
@@ -835,8 +838,7 @@ export class ExtensionUiController {
|
|
|
835
838
|
* Hide the hook editor.
|
|
836
839
|
*/
|
|
837
840
|
hideHookEditor(): void {
|
|
838
|
-
this
|
|
839
|
-
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
841
|
+
this.#restoreComposerEditor();
|
|
840
842
|
this.ctx.hookEditor = undefined;
|
|
841
843
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
842
844
|
this.ctx.ui.requestRender();
|
|
@@ -879,8 +881,7 @@ export class ExtensionUiController {
|
|
|
879
881
|
closed = true;
|
|
880
882
|
this.#clearActiveHookCustom();
|
|
881
883
|
if (!options?.overlay) {
|
|
882
|
-
this
|
|
883
|
-
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
884
|
+
this.#restoreComposerEditor();
|
|
884
885
|
this.ctx.editor.setText(savedText);
|
|
885
886
|
}
|
|
886
887
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
@@ -434,13 +434,14 @@ export class InputController {
|
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
const runner = this.ctx.session.extensionRunner;
|
|
437
|
+
const pendingImages = this.ctx.pendingImages;
|
|
437
438
|
let inputImages = this.#visiblePendingImagesForText(text);
|
|
438
439
|
|
|
439
440
|
if (runner?.hasHandlers("input")) {
|
|
440
441
|
const result = await runner.emitInput(text, inputImages, "interactive");
|
|
441
442
|
if (result?.handled) {
|
|
442
443
|
this.ctx.editor.setText("");
|
|
443
|
-
this
|
|
444
|
+
this.#clearPendingImagesIfOwnedBy(pendingImages);
|
|
444
445
|
return;
|
|
445
446
|
}
|
|
446
447
|
if (result?.text !== undefined) {
|
|
@@ -530,7 +531,7 @@ export class InputController {
|
|
|
530
531
|
this.ctx.editor.addToHistory(text);
|
|
531
532
|
this.ctx.editor.setText("");
|
|
532
533
|
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
533
|
-
this
|
|
534
|
+
this.#clearPendingImagesIfOwnedBy(pendingImages);
|
|
534
535
|
// Record the signature so the queued message's eventual delivery
|
|
535
536
|
// (a user-role `message_start` event) leaves any draft the user has
|
|
536
537
|
// typed since queuing intact. Same protection as #783, applied to
|
|
@@ -582,7 +583,7 @@ export class InputController {
|
|
|
582
583
|
if (this.ctx.onInputCallback) {
|
|
583
584
|
// Include any pending images from clipboard paste
|
|
584
585
|
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
585
|
-
this
|
|
586
|
+
this.#clearPendingImagesIfOwnedBy(pendingImages);
|
|
586
587
|
|
|
587
588
|
// Render user message immediately, then let session events catch up
|
|
588
589
|
const submission = this.ctx.startPendingSubmission({ text, images });
|
|
@@ -1104,11 +1105,14 @@ export class InputController {
|
|
|
1104
1105
|
return false;
|
|
1105
1106
|
}
|
|
1106
1107
|
|
|
1107
|
-
this.ctx.pendingImages
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1108
|
+
this.ctx.pendingImages = [
|
|
1109
|
+
...this.ctx.pendingImages,
|
|
1110
|
+
{
|
|
1111
|
+
type: "image",
|
|
1112
|
+
data: image.data,
|
|
1113
|
+
mimeType: image.mimeType,
|
|
1114
|
+
},
|
|
1115
|
+
];
|
|
1112
1116
|
this.ctx.editor.insertText(`${formatPastedImageReference(this.#nextImagePlaceholder(), image.resolvedPath)} `);
|
|
1113
1117
|
this.ctx.showStatus(`Attached image: ${path.basename(image.resolvedPath)}`, { dim: true });
|
|
1114
1118
|
this.ctx.ui.requestRender();
|
|
@@ -1151,10 +1155,33 @@ export class InputController {
|
|
|
1151
1155
|
return images.length > 0 ? images : undefined;
|
|
1152
1156
|
}
|
|
1153
1157
|
|
|
1158
|
+
#clearPendingImagesIfOwnedBy(pendingImages: InteractiveModeContext["pendingImages"]): void {
|
|
1159
|
+
if (this.ctx.pendingImages === pendingImages) {
|
|
1160
|
+
this.ctx.pendingImages = [];
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1154
1164
|
#clearPendingImagesIfPlaceholdersRemoved(text: string): void {
|
|
1155
1165
|
if (this.ctx.pendingImages.length === 0 || IMAGE_PLACEHOLDER_PRESENT_PATTERN.test(text)) {
|
|
1156
1166
|
return;
|
|
1157
1167
|
}
|
|
1168
|
+
// Editor submit resets the composer and emits onChange("") before onSubmit(result).
|
|
1169
|
+
// Defer the empty-buffer clear so the submit handler can still resolve image
|
|
1170
|
+
// placeholders against pendingImages, while manual clears still drop stale images.
|
|
1171
|
+
if (text.length === 0) {
|
|
1172
|
+
const pendingImages = this.ctx.pendingImages;
|
|
1173
|
+
const pendingImageCount = pendingImages.length;
|
|
1174
|
+
queueMicrotask(() => {
|
|
1175
|
+
if (
|
|
1176
|
+
this.ctx.pendingImages === pendingImages &&
|
|
1177
|
+
pendingImages.length === pendingImageCount &&
|
|
1178
|
+
this.ctx.editor.getText().length === 0
|
|
1179
|
+
) {
|
|
1180
|
+
this.ctx.pendingImages = [];
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1158
1185
|
this.ctx.pendingImages = [];
|
|
1159
1186
|
}
|
|
1160
1187
|
|
|
@@ -1185,11 +1212,14 @@ export class InputController {
|
|
|
1185
1212
|
}
|
|
1186
1213
|
}
|
|
1187
1214
|
|
|
1188
|
-
this.ctx.pendingImages
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1215
|
+
this.ctx.pendingImages = [
|
|
1216
|
+
...this.ctx.pendingImages,
|
|
1217
|
+
{
|
|
1218
|
+
type: "image",
|
|
1219
|
+
data: imageData.data,
|
|
1220
|
+
mimeType: imageData.mimeType,
|
|
1221
|
+
},
|
|
1222
|
+
];
|
|
1193
1223
|
this.ctx.editor.insertText(`${this.#nextImagePlaceholder()} `);
|
|
1194
1224
|
this.ctx.ui.requestRender();
|
|
1195
1225
|
return true;
|
|
@@ -79,6 +79,7 @@ import { HistorySearchComponent } from "../components/history-search";
|
|
|
79
79
|
import { JobsOverlayComponent } from "../components/jobs-overlay";
|
|
80
80
|
import { ModelSelectorComponent } from "../components/model-selector";
|
|
81
81
|
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
|
82
|
+
import { isPetAvailable } from "../components/pet-capability";
|
|
82
83
|
import { PetSelectorComponent } from "../components/pet-selector";
|
|
83
84
|
import { PluginSelectorComponent } from "../components/plugin-selector";
|
|
84
85
|
import {
|
|
@@ -492,6 +493,7 @@ export class SelectorController {
|
|
|
492
493
|
availableThemes,
|
|
493
494
|
availableModelProfiles: [...this.ctx.session.modelRegistry.getModelProfiles().keys()],
|
|
494
495
|
cwd: getProjectDir(),
|
|
496
|
+
petAvailable: isPetAvailable(),
|
|
495
497
|
},
|
|
496
498
|
{
|
|
497
499
|
onChange: (id, value) => this.handleSettingChange(id, value),
|
|
@@ -514,6 +516,7 @@ export class SelectorController {
|
|
|
514
516
|
onPetPreview: mode => {
|
|
515
517
|
this.ctx.previewPetMode(mode as PetMode);
|
|
516
518
|
},
|
|
519
|
+
onPetCommit: mode => this.ctx.commitPetPreviewMode(mode as PetMode),
|
|
517
520
|
onStatusLinePreview: previewSettings => {
|
|
518
521
|
// Update status line with preview settings
|
|
519
522
|
this.ctx.statusLine.updateSettings({
|
|
@@ -591,6 +594,7 @@ export class SelectorController {
|
|
|
591
594
|
showPetSelector(): void {
|
|
592
595
|
const stored = settings.get("pet.mode");
|
|
593
596
|
const initial: PetMode = isPetMode(stored) ? stored : "off";
|
|
597
|
+
const available = isPetAvailable();
|
|
594
598
|
this.showSelector(done => {
|
|
595
599
|
// Live-preview via previewMode (no editor re-mount, so the overlay stays);
|
|
596
600
|
// Enter commits + persists, Esc restores the initial skin.
|
|
@@ -607,6 +611,7 @@ export class SelectorController {
|
|
|
607
611
|
mode => {
|
|
608
612
|
this.ctx.previewPetMode(mode);
|
|
609
613
|
},
|
|
614
|
+
available,
|
|
610
615
|
);
|
|
611
616
|
return { component: selector, focus: selector.getSelectList() };
|
|
612
617
|
});
|
|
@@ -762,12 +767,6 @@ export class SelectorController {
|
|
|
762
767
|
});
|
|
763
768
|
break;
|
|
764
769
|
}
|
|
765
|
-
case "pet.mode":
|
|
766
|
-
// The settings submenu already persisted the value; apply it to the live
|
|
767
|
-
// widget via previewMode (the settings overlay is still open, so a full
|
|
768
|
-
// re-mount would tear it down — restoreComposer re-mounts on close).
|
|
769
|
-
this.ctx.previewPetMode(value as PetMode);
|
|
770
|
-
break;
|
|
771
770
|
case "symbolPreset": {
|
|
772
771
|
setSymbolPreset(value as "unicode" | "nerd" | "ascii").then(() => {
|
|
773
772
|
this.ctx.statusLine.invalidate();
|
|
@@ -985,21 +984,14 @@ export class SelectorController {
|
|
|
985
984
|
assignments,
|
|
986
985
|
});
|
|
987
986
|
if (!materializedProfile) {
|
|
988
|
-
const overrides = this.ctx.settings.get("task.agentModelOverrides");
|
|
989
|
-
const nextOverrides = { ...overrides };
|
|
990
|
-
let writesOverrides = false;
|
|
991
987
|
for (const targetRole of targetRoles) {
|
|
992
988
|
const target = GJC_MODEL_ASSIGNMENT_TARGETS[targetRole];
|
|
993
989
|
if (target.settingsPath === "modelRoles") {
|
|
994
990
|
this.ctx.settings.setModelRole(targetRole, value);
|
|
995
991
|
} else {
|
|
996
|
-
|
|
997
|
-
writesOverrides = true;
|
|
992
|
+
this.ctx.settings.setAgentModelOverride(targetRole, value);
|
|
998
993
|
}
|
|
999
994
|
}
|
|
1000
|
-
if (writesOverrides) {
|
|
1001
|
-
this.ctx.settings.set("task.agentModelOverrides", nextOverrides);
|
|
1002
|
-
}
|
|
1003
995
|
}
|
|
1004
996
|
modelSelector.refreshRoleAssignments({
|
|
1005
997
|
currentModel: this.ctx.session.model,
|
|
@@ -1069,10 +1061,7 @@ export class SelectorController {
|
|
|
1069
1061
|
if (target.settingsPath === "modelRoles") {
|
|
1070
1062
|
this.ctx.settings.setModelRole(role, value);
|
|
1071
1063
|
} else {
|
|
1072
|
-
this.ctx.settings.
|
|
1073
|
-
...this.ctx.settings.get("task.agentModelOverrides"),
|
|
1074
|
-
[role]: value,
|
|
1075
|
-
});
|
|
1064
|
+
this.ctx.settings.setAgentModelOverride(role, value);
|
|
1076
1065
|
}
|
|
1077
1066
|
}
|
|
1078
1067
|
modelSelector.refreshRoleAssignments({
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
getRenderCacheRetainedBytes,
|
|
22
22
|
Loader,
|
|
23
23
|
Markdown,
|
|
24
|
+
onImageProtocolChanged,
|
|
24
25
|
ProcessTerminal,
|
|
25
26
|
Spacer,
|
|
26
27
|
Text,
|
|
@@ -86,6 +87,12 @@ import type { HookEditorComponent } from "./components/hook-editor";
|
|
|
86
87
|
import type { HookInputComponent } from "./components/hook-input";
|
|
87
88
|
import type { HookSelectorComponent } from "./components/hook-selector";
|
|
88
89
|
import { IrcSplitViewComponent } from "./components/irc-sidebar";
|
|
90
|
+
import {
|
|
91
|
+
getPetUnavailableWarning,
|
|
92
|
+
isPetAvailable,
|
|
93
|
+
isPetCapabilityProbePending,
|
|
94
|
+
warnWhenPetCapabilitySettled,
|
|
95
|
+
} from "./components/pet-capability";
|
|
89
96
|
import { StatusLineComponent } from "./components/status-line";
|
|
90
97
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
91
98
|
import {
|
|
@@ -387,6 +394,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
387
394
|
#baseReservedSlashCommandNames: Set<string> = new Set();
|
|
388
395
|
#cleanupUnsubscribe?: () => void;
|
|
389
396
|
#subprocessTeardownUnsubscribe?: () => void;
|
|
397
|
+
#petProtocolUnsubscribe?: () => void;
|
|
398
|
+
/** Cancels a startup pet-unavailable warning still awaiting probe settlement. */
|
|
399
|
+
#petUnavailableWarningDisposer?: () => void;
|
|
390
400
|
readonly #version: string;
|
|
391
401
|
readonly #changelogMarkdown: string | undefined;
|
|
392
402
|
#planModePreviousTools: string[] | undefined;
|
|
@@ -615,8 +625,35 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
615
625
|
this.ui.addChild(this.hookWidgetContainerBelow);
|
|
616
626
|
this.ui.setBottomPinnedComponent(this.statusLine);
|
|
617
627
|
this.ui.setFocus(this.editor);
|
|
628
|
+
this.petWidget?.dispose();
|
|
618
629
|
this.petWidget = this.#createPetWidget(this.editor);
|
|
619
|
-
|
|
630
|
+
const configuredPetMode = settings.get("pet.mode");
|
|
631
|
+
this.petWidget.setMode(configuredPetMode);
|
|
632
|
+
// The async sixel capability probe can enable graphics after the saved
|
|
633
|
+
// pet mode was applied and dropped (no protocol yet at startup).
|
|
634
|
+
// Re-apply the configured mode when capability arrives so the pet
|
|
635
|
+
// appears without the user re-running /pet.
|
|
636
|
+
this.#petProtocolUnsubscribe?.();
|
|
637
|
+
this.#petProtocolUnsubscribe = onImageProtocolChanged(protocol => {
|
|
638
|
+
if (!protocol) return;
|
|
639
|
+
const saved = settings.get("pet.mode");
|
|
640
|
+
if (saved !== "off" && this.petWidget && this.petWidget.mode === "off") {
|
|
641
|
+
this.petWidget.setMode(saved);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
if (configuredPetMode !== "off" && !isPetAvailable()) {
|
|
645
|
+
// The async Sixel capability probe (started by TUI.start()) may still
|
|
646
|
+
// enable graphics; warn only once the capability question is settled
|
|
647
|
+
// so a supported terminal is never told it is incompatible.
|
|
648
|
+
this.#petUnavailableWarningDisposer?.();
|
|
649
|
+
this.#petUnavailableWarningDisposer = warnWhenPetCapabilitySettled({
|
|
650
|
+
probePending: isPetCapabilityProbePending(),
|
|
651
|
+
onUnavailable: () => {
|
|
652
|
+
this.showStatus(theme.fg("warning", getPetUnavailableWarning()), { dim: false });
|
|
653
|
+
this.ui.requestRender();
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
}
|
|
620
657
|
|
|
621
658
|
this.#inputController.setupKeyHandlers();
|
|
622
659
|
this.#inputController.setupEditorSubmitHandler();
|
|
@@ -1064,10 +1101,26 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1064
1101
|
this.editor.setTopBorder(undefined);
|
|
1065
1102
|
}
|
|
1066
1103
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1104
|
+
/**
|
|
1105
|
+
* Single result-returning pet commit policy shared by every entry path
|
|
1106
|
+
* (`/pet`, the pet selector, and the Settings submenu). Capability is
|
|
1107
|
+
* rechecked immediately before mutation, and the preference persists only
|
|
1108
|
+
* after the commit is accepted.
|
|
1109
|
+
*/
|
|
1110
|
+
#commitPetMode(mode: PetMode, apply: (mode: PetMode) => void): boolean {
|
|
1111
|
+
if (mode !== "off" && !isPetAvailable()) {
|
|
1112
|
+
this.showStatus(theme.fg("warning", getPetUnavailableWarning()), { dim: false });
|
|
1113
|
+
this.ui.requestRender();
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
apply(mode);
|
|
1069
1117
|
settings.set("pet.mode", mode);
|
|
1070
1118
|
this.ui.requestRender();
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
setPetMode(mode: PetMode): boolean {
|
|
1123
|
+
return this.#commitPetMode(mode, next => this.petWidget?.setMode(next));
|
|
1071
1124
|
}
|
|
1072
1125
|
|
|
1073
1126
|
previewPetMode(mode: PetMode): void {
|
|
@@ -1075,6 +1128,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1075
1128
|
this.ui.requestRender();
|
|
1076
1129
|
}
|
|
1077
1130
|
|
|
1131
|
+
commitPetPreviewMode(mode: PetMode): boolean {
|
|
1132
|
+
return this.#commitPetMode(mode, next => this.petWidget?.commitPreviewMode(next));
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1078
1135
|
restoreComposer(): void {
|
|
1079
1136
|
if (this.petWidget) {
|
|
1080
1137
|
this.petWidget.remountComposer();
|
|
@@ -2074,6 +2131,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2074
2131
|
}
|
|
2075
2132
|
|
|
2076
2133
|
stop(): void {
|
|
2134
|
+
this.#petProtocolUnsubscribe?.();
|
|
2135
|
+
this.#petProtocolUnsubscribe = undefined;
|
|
2136
|
+
this.#petUnavailableWarningDisposer?.();
|
|
2137
|
+
this.#petUnavailableWarningDisposer = undefined;
|
|
2077
2138
|
this.petWidget?.dispose();
|
|
2078
2139
|
this.petWidget = undefined;
|
|
2079
2140
|
if (this.loadingAnimation) {
|
|
@@ -2207,7 +2268,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2207
2268
|
nextEditor.setText(previousText);
|
|
2208
2269
|
previousEditor.dispose();
|
|
2209
2270
|
|
|
2210
|
-
const petMode =
|
|
2271
|
+
const petMode = settings.get("pet.mode");
|
|
2211
2272
|
this.petWidget?.dispose();
|
|
2212
2273
|
|
|
2213
2274
|
this.editorContainer.clear();
|