@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
package/src/main.ts
CHANGED
|
@@ -1398,12 +1398,10 @@ export async function runRootCommand(
|
|
|
1398
1398
|
if ($env.PI_TIMING) {
|
|
1399
1399
|
logger.printTimings();
|
|
1400
1400
|
}
|
|
1401
|
+
stopThemeWatcher();
|
|
1401
1402
|
if (!deps.suppressProcessExit) {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
await postmortem.quit(0);
|
|
1405
|
-
} else {
|
|
1406
|
-
stopThemeWatcher();
|
|
1403
|
+
const exitCode = typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
1404
|
+
await postmortem.quit(exitCode);
|
|
1407
1405
|
}
|
|
1408
1406
|
}
|
|
1409
1407
|
}
|
|
@@ -2217,14 +2217,14 @@ export class AcpAgent implements Agent {
|
|
|
2217
2217
|
const sessionId = record.session.sessionId;
|
|
2218
2218
|
|
|
2219
2219
|
const contextUsage = record.session.getContextUsage();
|
|
2220
|
-
if (contextUsage) {
|
|
2220
|
+
if (contextUsage?.tokens != null) {
|
|
2221
2221
|
const usageStats = record.session.sessionManager.getUsageStatistics();
|
|
2222
2222
|
await this.#connection.sessionUpdate({
|
|
2223
2223
|
sessionId,
|
|
2224
2224
|
update: {
|
|
2225
2225
|
sessionUpdate: "usage_update",
|
|
2226
2226
|
size: contextUsage.contextWindow,
|
|
2227
|
-
used: contextUsage.tokens
|
|
2227
|
+
used: contextUsage.tokens,
|
|
2228
2228
|
cost: usageStats.cost > 0 ? { amount: usageStats.cost, currency: "USD" } : undefined,
|
|
2229
2229
|
},
|
|
2230
2230
|
});
|
|
@@ -248,6 +248,7 @@ export class CustomEditor extends Editor {
|
|
|
248
248
|
const paste = this.#pasteHandler.process(data);
|
|
249
249
|
if (paste.handled) {
|
|
250
250
|
if (paste.pasteContent !== undefined) {
|
|
251
|
+
this.onViewportFollowLive?.();
|
|
251
252
|
this.#handleBracketedPaste(paste.pasteContent, paste.remaining);
|
|
252
253
|
}
|
|
253
254
|
return;
|
|
@@ -139,8 +139,8 @@ export class FooterComponent implements Component {
|
|
|
139
139
|
// After compaction, tokens are unknown until the next LLM response.
|
|
140
140
|
const contextUsage = this.session.getContextUsage();
|
|
141
141
|
const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
142
|
-
const contextPercentValue = contextUsage?.percent
|
|
143
|
-
const contextPercent =
|
|
142
|
+
const contextPercentValue = contextUsage?.percent;
|
|
143
|
+
const contextPercent = typeof contextPercentValue === "number" ? contextPercentValue.toFixed(1) : "?";
|
|
144
144
|
|
|
145
145
|
// Replace home directory with ~
|
|
146
146
|
let pwd = shortenPath(getProjectDir());
|
|
@@ -188,7 +188,7 @@ export class FooterComponent implements Component {
|
|
|
188
188
|
contextPercent === "?"
|
|
189
189
|
? `?/${formatNumber(contextWindow)}${autoIndicator}`
|
|
190
190
|
: `${contextPercent}%/${formatNumber(contextWindow)}${autoIndicator}`;
|
|
191
|
-
if (
|
|
191
|
+
if (typeof contextPercentValue === "number") {
|
|
192
192
|
const color = getContextUsageThemeColor(getContextUsageLevel(contextPercentValue, contextWindow));
|
|
193
193
|
contextPercentStr =
|
|
194
194
|
color === "statusLineContext" ? contextPercentDisplay : theme.fg(color, contextPercentDisplay);
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
type GajaePixelFrameName,
|
|
7
7
|
type GajaePixelFrames,
|
|
8
8
|
getCellDimensions,
|
|
9
|
-
ImageProtocol,
|
|
10
9
|
PARA_PARA_STEPS,
|
|
11
10
|
PET_SKINS,
|
|
12
11
|
type PetMode,
|
|
@@ -14,10 +13,10 @@ import {
|
|
|
14
13
|
petBurstDurationMs,
|
|
15
14
|
petBurstFrame,
|
|
16
15
|
registerAnimationCallback,
|
|
17
|
-
TERMINAL,
|
|
18
16
|
type TUI,
|
|
19
17
|
} from "@gajae-code/tui";
|
|
20
18
|
import type { CustomEditor } from "./custom-editor";
|
|
19
|
+
import { getPetPixelProtocol } from "./pet-capability";
|
|
21
20
|
|
|
22
21
|
/** Re-exported from the tui skin registry so widget-relative imports stay valid. */
|
|
23
22
|
export type { PetMode, PetSkinId };
|
|
@@ -38,6 +37,35 @@ const KITTY_DROP_FRACTION = 0.45;
|
|
|
38
37
|
const petKittyDropPx = (cellHeightPx: number): number =>
|
|
39
38
|
Math.min(Math.max(0, cellHeightPx - 1), Math.floor(cellHeightPx * KITTY_DROP_FRACTION));
|
|
40
39
|
const PET_RAISE_ROWS = 1;
|
|
40
|
+
const allocatedPetKittyImageIds = new Set<number>();
|
|
41
|
+
|
|
42
|
+
function allocatePetKittyImageId(): number {
|
|
43
|
+
let id = 0;
|
|
44
|
+
while (id === 0 || allocatedPetKittyImageIds.has(id)) {
|
|
45
|
+
id = crypto.getRandomValues(new Uint32Array(1))[0] ?? 0;
|
|
46
|
+
}
|
|
47
|
+
allocatedPetKittyImageIds.add(id);
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface SixelFootprint {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
columns: number;
|
|
55
|
+
rows: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sameFootprint(left: SixelFootprint, right: SixelFootprint): boolean {
|
|
59
|
+
return left.x === right.x && left.y === right.y && left.columns === right.columns && left.rows === right.rows;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Which widget currently owns each TUI's single shared post-render emitter
|
|
64
|
+
* slot. A stale or repeated dispose (or off-switch) of a predecessor widget
|
|
65
|
+
* must never clear a successor's overlay authority.
|
|
66
|
+
*/
|
|
67
|
+
const petOverlayEmitterOwners = new WeakMap<TUI, GajaePetWidget>();
|
|
68
|
+
|
|
41
69
|
/** Working animation: the shared para-para beats looped end to end. */
|
|
42
70
|
const WORK_LOOP_TOTAL = PARA_PARA_STEPS.reduce((sum, [, ms]) => sum + ms, 0);
|
|
43
71
|
/** Random gap between automatic claw flexes (fires while idle AND working). */
|
|
@@ -127,6 +155,20 @@ export class GajaePetWidget {
|
|
|
127
155
|
/** Cell metrics the current frames were built for; a change triggers a rebuild. */
|
|
128
156
|
#builtCellW = 0;
|
|
129
157
|
#builtCellH = 0;
|
|
158
|
+
#kittyImageId: number | undefined;
|
|
159
|
+
/** True while a kitty placement may exist on screen; cleared only after the delete escape is delivered. */
|
|
160
|
+
#kittyCleanupPending = false;
|
|
161
|
+
/** Last emitted Sixel raster position; retained until an erase is actually delivered. */
|
|
162
|
+
#lastSixelFootprint: SixelFootprint | undefined;
|
|
163
|
+
/** Terminal state: a disposed widget never touches the TUI or shared slots again. */
|
|
164
|
+
#disposed = false;
|
|
165
|
+
/**
|
|
166
|
+
* True while the previous overlay frame carried the cleanup payload. The
|
|
167
|
+
* TUI writes the frame after the emitter returns, so delivery is
|
|
168
|
+
* acknowledged only on the next emitter pass — and only while the terminal
|
|
169
|
+
* stayed available, since a failed render write drops availability.
|
|
170
|
+
*/
|
|
171
|
+
#frameCleanupAwaitingAck = false;
|
|
130
172
|
|
|
131
173
|
constructor(options: {
|
|
132
174
|
ui: TUI;
|
|
@@ -154,9 +196,7 @@ export class GajaePetWidget {
|
|
|
154
196
|
|
|
155
197
|
/** Protocol available for the real-pixel pet, if any. */
|
|
156
198
|
static pixelProtocol(): "sixel" | "kitty" | null {
|
|
157
|
-
|
|
158
|
-
if (TERMINAL.imageProtocol === ImageProtocol.Sixel) return "sixel";
|
|
159
|
-
return null;
|
|
199
|
+
return getPetPixelProtocol();
|
|
160
200
|
}
|
|
161
201
|
|
|
162
202
|
get mode(): PetMode {
|
|
@@ -182,14 +222,19 @@ export class GajaePetWidget {
|
|
|
182
222
|
}
|
|
183
223
|
}
|
|
184
224
|
|
|
225
|
+
commitPreviewMode(mode: PetMode): void {
|
|
226
|
+
this.#applyMode(mode, false);
|
|
227
|
+
}
|
|
228
|
+
|
|
185
229
|
#applyMode(mode: PetMode, mountComposer: boolean): void {
|
|
186
|
-
if (mode === this.#mode) return;
|
|
230
|
+
if (this.#disposed || mode === this.#mode) return;
|
|
187
231
|
|
|
188
232
|
if (mode === "off") {
|
|
233
|
+
this.#writeImageCleanup();
|
|
189
234
|
this.#mode = "off";
|
|
190
235
|
this.#animation?.unregister();
|
|
191
236
|
this.#animation = undefined;
|
|
192
|
-
this.#
|
|
237
|
+
this.#releaseOverlayEmitter();
|
|
193
238
|
this.#floorContainer.clear();
|
|
194
239
|
this.#pixel = undefined;
|
|
195
240
|
this.#framedEditor.setReserve(0);
|
|
@@ -200,6 +245,7 @@ export class GajaePetWidget {
|
|
|
200
245
|
|
|
201
246
|
const protocol = this.#forcedProtocol ?? GajaePetWidget.pixelProtocol();
|
|
202
247
|
if (!protocol) return;
|
|
248
|
+
if (this.#mode !== "off") this.#writeImageCleanup();
|
|
203
249
|
this.#mode = mode;
|
|
204
250
|
this.#frame = "base";
|
|
205
251
|
this.#flexUntil = 0;
|
|
@@ -210,6 +256,7 @@ export class GajaePetWidget {
|
|
|
210
256
|
// the composer stays pinned to the terminal bottom.
|
|
211
257
|
this.#floorContainer.clear();
|
|
212
258
|
this.#ui.setPostRenderEmitter(() => this.#overlayPayload());
|
|
259
|
+
petOverlayEmitterOwners.set(this.#ui, this);
|
|
213
260
|
this.#animation ??= registerAnimationCallback(now => this.#tick(now), 80);
|
|
214
261
|
this.#ui.requestRender(true);
|
|
215
262
|
}
|
|
@@ -220,6 +267,10 @@ export class GajaePetWidget {
|
|
|
220
267
|
this.#builtCellW = cell.widthPx;
|
|
221
268
|
this.#builtCellH = cell.heightPx;
|
|
222
269
|
const skin: PetSkinId = this.#mode === "off" ? "red" : this.#mode;
|
|
270
|
+
if (protocol === "kitty") {
|
|
271
|
+
this.#kittyImageId ??= allocatePetKittyImageId();
|
|
272
|
+
this.#kittyCleanupPending = true;
|
|
273
|
+
}
|
|
223
274
|
this.#pixel = buildGajaePixelFrames({
|
|
224
275
|
protocol,
|
|
225
276
|
skin,
|
|
@@ -228,17 +279,49 @@ export class GajaePetWidget {
|
|
|
228
279
|
targetRows: 2,
|
|
229
280
|
sixelTopPaddingPx: protocol === "sixel" ? PET_SIXEL_DROP_PX : 0,
|
|
230
281
|
kittyCellYOffsetPx: protocol === "kitty" ? petKittyDropPx(cell.heightPx) : 0,
|
|
282
|
+
kittyImageId: protocol === "kitty" ? this.#kittyImageId : undefined,
|
|
231
283
|
});
|
|
232
284
|
this.#framedEditor.setReserve(this.#pixel.columns + PET_SIDE_MARGIN);
|
|
233
285
|
}
|
|
234
286
|
|
|
235
287
|
dispose(): void {
|
|
236
|
-
this.#
|
|
237
|
-
this.#
|
|
238
|
-
this.#
|
|
239
|
-
this.#
|
|
240
|
-
|
|
241
|
-
|
|
288
|
+
if (this.#disposed) return;
|
|
289
|
+
this.#disposed = true;
|
|
290
|
+
const kittyImageId = this.#kittyImageId;
|
|
291
|
+
const cleanupPayload = this.#imageCleanupPayload();
|
|
292
|
+
try {
|
|
293
|
+
if (cleanupPayload) {
|
|
294
|
+
this.#ui.queueTerminalCleanup(
|
|
295
|
+
`\x1b[?2026h\x1b7${cleanupPayload}\x1b8\x1b[?2026l`,
|
|
296
|
+
kittyImageId === undefined ? undefined : () => allocatedPetKittyImageIds.delete(kittyImageId),
|
|
297
|
+
);
|
|
298
|
+
} else if (kittyImageId !== undefined) {
|
|
299
|
+
allocatedPetKittyImageIds.delete(kittyImageId);
|
|
300
|
+
}
|
|
301
|
+
this.#consumeCleanupAuthority();
|
|
302
|
+
this.#kittyImageId = undefined;
|
|
303
|
+
} finally {
|
|
304
|
+
this.#animation?.unregister();
|
|
305
|
+
this.#animation = undefined;
|
|
306
|
+
this.#releaseOverlayEmitter();
|
|
307
|
+
this.#mode = "off";
|
|
308
|
+
this.#pixel = undefined;
|
|
309
|
+
this.#floorContainer.clear();
|
|
310
|
+
this.#framedEditor.setReserve(0);
|
|
311
|
+
// Restore the plain composer only while our framed wrapper is still
|
|
312
|
+
// mounted; a successor widget may already own the editor container.
|
|
313
|
+
if (this.#editorContainer.children.includes(this.#framedEditor)) {
|
|
314
|
+
this.#mountEditor(false);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Clear the shared post-render slot only while this widget still owns it. */
|
|
320
|
+
#releaseOverlayEmitter(): void {
|
|
321
|
+
if (petOverlayEmitterOwners.get(this.#ui) === this) {
|
|
322
|
+
this.#ui.setPostRenderEmitter(undefined);
|
|
323
|
+
petOverlayEmitterOwners.delete(this.#ui);
|
|
324
|
+
}
|
|
242
325
|
}
|
|
243
326
|
|
|
244
327
|
#mountEditor(framed: boolean): void {
|
|
@@ -335,25 +418,91 @@ export class GajaePetWidget {
|
|
|
335
418
|
return { x, y };
|
|
336
419
|
}
|
|
337
420
|
|
|
338
|
-
#
|
|
339
|
-
const pixel = this.#pixel;
|
|
340
|
-
if (pixel?.protocol !== "sixel") return "";
|
|
421
|
+
#clearSixelFootprint(footprint: SixelFootprint): string {
|
|
341
422
|
let out = "\x1b[0m";
|
|
342
|
-
for (let row = 0; row <
|
|
343
|
-
out += `\x1b[${y + row + 1};${x + 1}H\x1b[${
|
|
423
|
+
for (let row = 0; row < footprint.rows; row++) {
|
|
424
|
+
out += `\x1b[${footprint.y + row + 1};${footprint.x + 1}H\x1b[${footprint.columns}X`;
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Pending on-screen image cleanup. Pure: authority is consumed separately, on delivery. */
|
|
430
|
+
#imageCleanupPayload(): string {
|
|
431
|
+
let out = "";
|
|
432
|
+
if (this.#kittyCleanupPending && this.#kittyImageId !== undefined) {
|
|
433
|
+
out += `\x1b_Ga=d,d=I,i=${this.#kittyImageId},q=2\x1b\\`;
|
|
434
|
+
}
|
|
435
|
+
if (this.#lastSixelFootprint) {
|
|
436
|
+
out += this.#clearSixelFootprint(this.#lastSixelFootprint);
|
|
344
437
|
}
|
|
345
438
|
return out;
|
|
346
439
|
}
|
|
347
440
|
|
|
441
|
+
#consumeCleanupAuthority(): void {
|
|
442
|
+
this.#kittyCleanupPending = false;
|
|
443
|
+
this.#lastSixelFootprint = undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Best-effort direct erase of the on-screen pet image. Cleanup authority is
|
|
448
|
+
* consumed only after the write is actually delivered: an unavailable
|
|
449
|
+
* terminal or a throwing write keeps the erase pending so a later mode
|
|
450
|
+
* switch or dispose can retry it.
|
|
451
|
+
*/
|
|
452
|
+
#writeImageCleanup(): void {
|
|
453
|
+
if (!this.#ui.terminalAvailable) return;
|
|
454
|
+
const payload = this.#imageCleanupPayload();
|
|
455
|
+
if (!payload) return;
|
|
456
|
+
try {
|
|
457
|
+
this.#ui.terminal.write(`\x1b[?2026h\x1b7${payload}\x1b8\x1b[?2026l`);
|
|
458
|
+
} catch {
|
|
459
|
+
// Keep the footprint/placement authority; the terminal write layer
|
|
460
|
+
// reports availability separately and callers retry on the next
|
|
461
|
+
// lifecycle transition.
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
this.#consumeCleanupAuthority();
|
|
465
|
+
}
|
|
466
|
+
|
|
348
467
|
/** Draw escape payload at the pet's absolute position. */
|
|
349
468
|
#overlayPayload(clearPet = false): string | null {
|
|
350
469
|
const pixel = this.#pixel;
|
|
351
470
|
if (!pixel) return null;
|
|
352
471
|
const pos = this.#petPosition();
|
|
353
|
-
if (!pos)
|
|
472
|
+
if (!pos) {
|
|
473
|
+
// Deferred delivery acknowledgement: the TUI writes the frame after
|
|
474
|
+
// this emitter returns, and that write can fail. Consume the cleanup
|
|
475
|
+
// authority only once a later pass observes the terminal survived
|
|
476
|
+
// the frame that carried the payload; otherwise retain it so a later
|
|
477
|
+
// lifecycle cleanup retries the erase/delete.
|
|
478
|
+
if (this.#frameCleanupAwaitingAck && this.#ui.terminalAvailable) {
|
|
479
|
+
this.#consumeCleanupAuthority();
|
|
480
|
+
}
|
|
481
|
+
this.#frameCleanupAwaitingAck = false;
|
|
482
|
+
if (!this.#ui.terminalAvailable) return null;
|
|
483
|
+
const cleanup = this.#imageCleanupPayload();
|
|
484
|
+
if (!cleanup) return null;
|
|
485
|
+
this.#frameCleanupAwaitingAck = true;
|
|
486
|
+
return cleanup;
|
|
487
|
+
}
|
|
488
|
+
// A full frame supersedes any cleanup-only frame still awaiting ack.
|
|
489
|
+
this.#frameCleanupAwaitingAck = false;
|
|
354
490
|
const { x, y } = pos;
|
|
491
|
+
let out = "";
|
|
492
|
+
|
|
493
|
+
if (pixel.protocol === "sixel") {
|
|
494
|
+
const footprint = { x, y, columns: pixel.columns, rows: pixel.rasterRows };
|
|
495
|
+
if (this.#lastSixelFootprint && !sameFootprint(this.#lastSixelFootprint, footprint)) {
|
|
496
|
+
out += this.#clearSixelFootprint(this.#lastSixelFootprint);
|
|
497
|
+
}
|
|
498
|
+
if (clearPet) out += this.#clearSixelFootprint(footprint);
|
|
499
|
+
this.#lastSixelFootprint = footprint;
|
|
500
|
+
} else {
|
|
501
|
+
// A kitty frame emitted below (re)places the image, so cleanup is
|
|
502
|
+
// pending again even if a narrow-terminal pass consumed it earlier.
|
|
503
|
+
this.#kittyCleanupPending = true;
|
|
504
|
+
}
|
|
355
505
|
|
|
356
|
-
let out = clearPet ? this.#clearPetPayload(x, y) : "";
|
|
357
506
|
out += `\x1b[${y + 1};${x + 1}H${pixel.frames[this.#frame]}`;
|
|
358
507
|
return out;
|
|
359
508
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ImageProtocol,
|
|
3
|
+
isUnderTerminalMultiplexer,
|
|
4
|
+
onImageProtocolChanged,
|
|
5
|
+
type SelectItem,
|
|
6
|
+
shouldProbeSixelCapability,
|
|
7
|
+
TERMINAL,
|
|
8
|
+
} from "@gajae-code/tui";
|
|
9
|
+
|
|
10
|
+
export type PetPixelProtocol = "sixel" | "kitty";
|
|
11
|
+
|
|
12
|
+
export const PET_UNAVAILABLE_DESCRIPTION = "Unavailable: requires compatible Kitty or Sixel overlay rendering";
|
|
13
|
+
export const PET_SAVED_UNAVAILABLE_DESCRIPTION =
|
|
14
|
+
"Saved, unavailable — requires compatible Kitty or Sixel overlay rendering";
|
|
15
|
+
export const PET_UNAVAILABLE_WARNING =
|
|
16
|
+
"⚠ Pets aren’t available in this terminal. Its image support isn’t compatible with Gajae Pet’s overlay rendering yet. Try Kitty, Ghostty, WezTerm, or a terminal with compatible Sixel support.";
|
|
17
|
+
const PET_MULTIPLEXER_UNAVAILABLE_WARNING =
|
|
18
|
+
"⚠ Gajae Pet graphics are unavailable inside tmux, screen, or zellij because image escapes are not forwarded end to end. Run gjc outside the multiplexer, or set PI_FORCE_IMAGE_PROTOCOL=sixel only when the full terminal chain supports Sixel.";
|
|
19
|
+
|
|
20
|
+
export function getPetUnavailableWarning(env: NodeJS.ProcessEnv = Bun.env): string {
|
|
21
|
+
return isUnderTerminalMultiplexer(env) ? PET_MULTIPLEXER_UNAVAILABLE_WARNING : PET_UNAVAILABLE_WARNING;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getPetPixelProtocol(): PetPixelProtocol | null {
|
|
25
|
+
if (TERMINAL.imageProtocol === ImageProtocol.Kitty) return "kitty";
|
|
26
|
+
if (TERMINAL.imageProtocol === ImageProtocol.Sixel) return "sixel";
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isPetAvailable(): boolean {
|
|
31
|
+
return getPetPixelProtocol() !== null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createPetSelectItems(
|
|
35
|
+
options: ReadonlyArray<SelectItem>,
|
|
36
|
+
currentValue: string,
|
|
37
|
+
available: boolean,
|
|
38
|
+
): SelectItem[] {
|
|
39
|
+
return options.map(option => {
|
|
40
|
+
const disabled = !available && option.value !== "off";
|
|
41
|
+
const current = option.value === currentValue;
|
|
42
|
+
const savedUnavailable = disabled && current;
|
|
43
|
+
let description = `${option.description ?? ""}${current ? " (current)" : ""}`;
|
|
44
|
+
if (disabled) {
|
|
45
|
+
description = savedUnavailable ? PET_SAVED_UNAVAILABLE_DESCRIPTION : PET_UNAVAILABLE_DESCRIPTION;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...option,
|
|
49
|
+
label: savedUnavailable ? `${option.label} (saved)` : option.label,
|
|
50
|
+
description,
|
|
51
|
+
disabled,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Grace period before declaring the terminal pet-incapable at startup. The
|
|
58
|
+
* asynchronous Sixel capability probe starts inside `TUI.start()` and answers
|
|
59
|
+
* within its own 250 ms deadline; this margin covers probe scheduling so a
|
|
60
|
+
* supported terminal is never told it is incompatible while the probe is
|
|
61
|
+
* still in flight.
|
|
62
|
+
*/
|
|
63
|
+
export const PET_CAPABILITY_SETTLE_MS = 1_000;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Whether the asynchronous startup Sixel capability probe may still enable
|
|
67
|
+
* graphics for this session, meaning current unavailability is not final.
|
|
68
|
+
*/
|
|
69
|
+
export function isPetCapabilityProbePending(
|
|
70
|
+
env: NodeJS.ProcessEnv = Bun.env,
|
|
71
|
+
platform: NodeJS.Platform = process.platform,
|
|
72
|
+
): boolean {
|
|
73
|
+
if (TERMINAL.imageProtocol !== null) return false;
|
|
74
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
75
|
+
return shouldProbeSixelCapability(env, platform);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Deliver the pet-unavailable startup warning only once the capability
|
|
80
|
+
* question is settled. With no probe pending, unavailability is final and
|
|
81
|
+
* `onUnavailable` fires immediately. While a probe may still enable
|
|
82
|
+
* graphics, the warning is deferred: a protocol-change event cancels it (the
|
|
83
|
+
* saved pet re-applies through the existing subscription), and only the
|
|
84
|
+
* settle deadline passing with the terminal still unavailable emits it.
|
|
85
|
+
* Returns a disposer that cancels the pending decision.
|
|
86
|
+
*/
|
|
87
|
+
export function warnWhenPetCapabilitySettled(options: {
|
|
88
|
+
probePending: boolean;
|
|
89
|
+
isAvailable?: () => boolean;
|
|
90
|
+
onUnavailable: () => void;
|
|
91
|
+
settleMs?: number;
|
|
92
|
+
}): () => void {
|
|
93
|
+
if (!options.probePending) {
|
|
94
|
+
options.onUnavailable();
|
|
95
|
+
return () => {};
|
|
96
|
+
}
|
|
97
|
+
const isAvailable = options.isAvailable ?? isPetAvailable;
|
|
98
|
+
let settled = false;
|
|
99
|
+
const finish = () => {
|
|
100
|
+
if (settled) return;
|
|
101
|
+
settled = true;
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
unsubscribe();
|
|
104
|
+
};
|
|
105
|
+
const unsubscribe = onImageProtocolChanged(protocol => {
|
|
106
|
+
if (!protocol) return;
|
|
107
|
+
finish();
|
|
108
|
+
});
|
|
109
|
+
const timer = setTimeout(() => {
|
|
110
|
+
finish();
|
|
111
|
+
if (!isAvailable()) options.onUnavailable();
|
|
112
|
+
}, options.settleMs ?? PET_CAPABILITY_SETTLE_MS);
|
|
113
|
+
timer.unref?.();
|
|
114
|
+
return finish;
|
|
115
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Container, PET_SKIN_IDS, PET_SKINS,
|
|
1
|
+
import { Container, PET_SKIN_IDS, PET_SKINS, SelectList } from "@gajae-code/tui";
|
|
2
2
|
import { getSelectListTheme } from "../theme/theme";
|
|
3
3
|
import { DynamicBorder } from "./dynamic-border";
|
|
4
4
|
import type { PetMode } from "./gajae-pet-widget";
|
|
5
|
+
import { createPetSelectItems } from "./pet-capability";
|
|
5
6
|
|
|
6
7
|
const PET_OPTIONS: { value: PetMode; label: string; description: string }[] = [
|
|
7
8
|
{ value: "off", label: "Off", description: "No pet" },
|
|
@@ -24,14 +25,11 @@ export class PetSelectorComponent extends Container {
|
|
|
24
25
|
onSelect: (mode: PetMode) => void,
|
|
25
26
|
onCancel: () => void,
|
|
26
27
|
onPreview: (mode: PetMode) => void,
|
|
28
|
+
available: boolean,
|
|
27
29
|
) {
|
|
28
30
|
super();
|
|
29
31
|
|
|
30
|
-
const items
|
|
31
|
-
value: option.value,
|
|
32
|
-
label: option.label,
|
|
33
|
-
description: option.value === current ? `${option.description} (current)` : option.description,
|
|
34
|
-
}));
|
|
32
|
+
const items = createPetSelectItems(PET_OPTIONS, current, available);
|
|
35
33
|
|
|
36
34
|
this.addChild(new DynamicBorder());
|
|
37
35
|
|
|
@@ -25,6 +25,7 @@ import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme }
|
|
|
25
25
|
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
|
26
26
|
import { getTabBarTheme } from "../shared";
|
|
27
27
|
import { DynamicBorder } from "./dynamic-border";
|
|
28
|
+
import { createPetSelectItems, getPetUnavailableWarning, isPetAvailable } from "./pet-capability";
|
|
28
29
|
import { handleInputOrEscape, PluginSettingsComponent } from "./plugin-settings";
|
|
29
30
|
import { getSettingsForTab, type SettingDef } from "./settings-defs";
|
|
30
31
|
import type { StatusLineSegmentOptions } from "./status-line";
|
|
@@ -661,6 +662,8 @@ export interface SettingsRuntimeContext {
|
|
|
661
662
|
availableModelProfiles: string[];
|
|
662
663
|
/** Working directory for plugins tab */
|
|
663
664
|
cwd: string;
|
|
665
|
+
/** Whether this terminal can render the pet overlay. */
|
|
666
|
+
petAvailable?: boolean;
|
|
664
667
|
}
|
|
665
668
|
|
|
666
669
|
/** Status line settings subset for preview */
|
|
@@ -684,6 +687,13 @@ export interface SettingsCallbacks {
|
|
|
684
687
|
onThemePreviewCancel?: (theme: string) => void | Promise<void>;
|
|
685
688
|
/** Called to live-preview the gajae pet skin while browsing the pet setting. */
|
|
686
689
|
onPetPreview?: (mode: string) => void;
|
|
690
|
+
/**
|
|
691
|
+
* Commit a pet mode through the shared result-returning policy. The policy
|
|
692
|
+
* rechecks capability immediately before mutation and owns persistence, so
|
|
693
|
+
* the settings surface never persists `pet.mode` ahead of acceptance.
|
|
694
|
+
* Returns whether the commit was accepted.
|
|
695
|
+
*/
|
|
696
|
+
onPetCommit?: (mode: string) => boolean;
|
|
687
697
|
/** Called for status line preview while configuring */
|
|
688
698
|
onStatusLinePreview?: (settings: StatusLinePreviewSettings) => void;
|
|
689
699
|
/** Get current rendered status line for inline preview */
|
|
@@ -841,7 +851,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
841
851
|
currentValue: string,
|
|
842
852
|
done: (value?: string) => void,
|
|
843
853
|
): Container {
|
|
844
|
-
let options = def.options;
|
|
854
|
+
let options: ReadonlyArray<SelectItem> = def.options;
|
|
845
855
|
|
|
846
856
|
// Special case: inject runtime options for thinking level
|
|
847
857
|
if (def.path === "defaultThinkingLevel") {
|
|
@@ -857,6 +867,14 @@ export class SettingsSelectorComponent extends Container {
|
|
|
857
867
|
if (def.path === "statusLine.preset") {
|
|
858
868
|
options = options.filter(option => option.value !== "custom");
|
|
859
869
|
}
|
|
870
|
+
let description = def.description;
|
|
871
|
+
if (def.path === "pet.mode") {
|
|
872
|
+
const petAvailable = this.context.petAvailable ?? isPetAvailable();
|
|
873
|
+
options = createPetSelectItems(options, currentValue, petAvailable);
|
|
874
|
+
// Unsupported terminals must see the same actionable guidance the
|
|
875
|
+
// startup notice and /pet show, not only dimmed option descriptions.
|
|
876
|
+
if (!petAvailable) description = getPetUnavailableWarning();
|
|
877
|
+
}
|
|
860
878
|
// Preview handlers
|
|
861
879
|
let onPreview: ((value: string) => void | Promise<void>) | undefined;
|
|
862
880
|
let onPreviewCancel: (() => void) | undefined;
|
|
@@ -943,10 +961,18 @@ export class SettingsSelectorComponent extends Container {
|
|
|
943
961
|
|
|
944
962
|
return new SelectSubmenu(
|
|
945
963
|
def.label,
|
|
946
|
-
|
|
964
|
+
description,
|
|
947
965
|
options,
|
|
948
966
|
currentValue,
|
|
949
967
|
value => {
|
|
968
|
+
if (def.path === "pet.mode") {
|
|
969
|
+
// The shared pet commit policy rechecks capability immediately
|
|
970
|
+
// before mutation and persists only on acceptance; the settings
|
|
971
|
+
// surface must not persist ahead of that result.
|
|
972
|
+
const accepted = this.callbacks.onPetCommit?.(value) ?? false;
|
|
973
|
+
done(accepted ? value : undefined);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
950
976
|
this.#setSettingValue(def.path, value);
|
|
951
977
|
this.callbacks.onChange(def.path, value);
|
|
952
978
|
done(value);
|
|
@@ -114,9 +114,13 @@ const modelSegment: StatusLineSegment = {
|
|
|
114
114
|
if (opts.showContextPercent !== false && ctx.contextPctSegmentActive !== true) {
|
|
115
115
|
const pct = ctx.contextPercent;
|
|
116
116
|
const window = ctx.contextWindow;
|
|
117
|
-
if (window > 0
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
if (window > 0) {
|
|
118
|
+
if (typeof pct === "number" && Number.isFinite(pct)) {
|
|
119
|
+
const color = getContextUsageThemeColor(getContextUsageLevel(pct, window));
|
|
120
|
+
content += `${theme.sep.dot}${theme.fg(color, `${pct.toFixed(1)}%`)}`;
|
|
121
|
+
} else {
|
|
122
|
+
content += `${theme.sep.dot}${theme.fg("statusLineContext", "?")}`;
|
|
123
|
+
}
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
|
|
@@ -382,11 +386,14 @@ const contextPctSegment: StatusLineSegment = {
|
|
|
382
386
|
render(ctx) {
|
|
383
387
|
const pct = ctx.contextPercent;
|
|
384
388
|
const window = ctx.contextWindow;
|
|
389
|
+
const knownPct = typeof pct === "number" && Number.isFinite(pct) ? pct : undefined;
|
|
385
390
|
|
|
386
391
|
const autoIcon = ctx.autoCompactEnabled && theme.icon.auto ? ` ${theme.icon.auto}` : "";
|
|
387
|
-
const text = `${
|
|
388
|
-
|
|
389
|
-
|
|
392
|
+
const text = `${knownPct === undefined ? "?" : `${knownPct.toFixed(1)}%`}/${formatNumber(window)}${autoIcon}`;
|
|
393
|
+
const color =
|
|
394
|
+
knownPct === undefined
|
|
395
|
+
? "statusLineContext"
|
|
396
|
+
: getContextUsageThemeColor(getContextUsageLevel(knownPct, window));
|
|
390
397
|
const content = withIcon(theme.icon.context, theme.fg(color, text));
|
|
391
398
|
|
|
392
399
|
return { content, visible: true };
|
|
@@ -39,7 +39,7 @@ export interface SegmentContext {
|
|
|
39
39
|
cost: number;
|
|
40
40
|
tokensPerSecond: number | null;
|
|
41
41
|
};
|
|
42
|
-
contextPercent: number;
|
|
42
|
+
contextPercent: number | null;
|
|
43
43
|
contextWindow: number;
|
|
44
44
|
/**
|
|
45
45
|
* True when a standalone `context_pct` segment is also part of the active
|