@gajae-code/coding-agent 0.13.1 → 0.13.3
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 +41 -0
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
- package/dist/types/modes/components/gajae-pet-widget.d.ts +5 -5
- package/dist/types/modes/components/pet-capability.d.ts +5 -5
- package/dist/types/modes/components/settings-selector.d.ts +5 -0
- package/dist/types/modes/theme/defaults/index.d.ts +118 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +13 -0
- package/dist/types/session/session-manager.d.ts +7 -0
- package/dist/types/tools/image-gen.d.ts +1 -0
- package/package.json +8 -8
- package/src/config/model-profile-activation.ts +3 -1
- package/src/config/model-profiles.ts +59 -2
- package/src/config/model-registry.ts +16 -1
- package/src/config/settings-schema.ts +1 -1
- package/src/coordinator-mcp/server.ts +3 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/main.ts +4 -0
- package/src/modes/components/chrome.ts +4 -3
- package/src/modes/components/dynamic-theme-text.ts +22 -0
- package/src/modes/components/gajae-pet-widget.ts +130 -42
- package/src/modes/components/model-selector.ts +1 -6
- package/src/modes/components/pet-capability.ts +10 -5
- package/src/modes/components/settings-selector.ts +20 -5
- package/src/modes/components/status-line/segments.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +61 -5
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/defaults/ouroboros.json +118 -0
- package/src/sdk/broker/process-incarnation.ts +6 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/bus/telegram-daemon.ts +51 -43
- package/src/session/agent-session.ts +39 -0
- package/src/session/internal/managed-session-storage.ts +162 -40
- package/src/session/session-manager.ts +28 -0
- package/src/slash-commands/builtin-registry.ts +62 -8
- package/src/task/index.ts +5 -2
- package/src/tools/bash.ts +11 -9
- package/src/tools/image-gen.ts +322 -126
- package/src/tools/todo-contract.ts +55 -3
- package/src/tools/todo-write.ts +4 -3
package/src/main.ts
CHANGED
|
@@ -1255,6 +1255,7 @@ export interface RunRootCommandDependencies {
|
|
|
1255
1255
|
getChangelogForDisplay?: typeof getChangelogForDisplay;
|
|
1256
1256
|
createInteractiveMode?: CreateInteractiveMode;
|
|
1257
1257
|
runPrintMode?: RunPrintMode;
|
|
1258
|
+
quit?: (code: number) => Promise<void>;
|
|
1258
1259
|
isResumePickerTerminal?: ResumePickerTerminalCheck;
|
|
1259
1260
|
listForResumePickerReadOnly?: ListForResumePickerReadOnly;
|
|
1260
1261
|
listManagedForResumePickerReadOnly?: ListManagedForResumePickerReadOnly;
|
|
@@ -1961,6 +1962,9 @@ export async function runRootCommand(
|
|
|
1961
1962
|
authStorage.close();
|
|
1962
1963
|
if (!deps.suppressProcessExit) await postmortem.cleanup();
|
|
1963
1964
|
}
|
|
1965
|
+
if (!deps.suppressProcessExit) {
|
|
1966
|
+
await (deps.quit ?? postmortem.quit)(typeof process.exitCode === "number" ? process.exitCode : 0);
|
|
1967
|
+
}
|
|
1964
1968
|
}
|
|
1965
1969
|
}
|
|
1966
1970
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Container, type SelectItem, SelectList
|
|
1
|
+
import { Container, type SelectItem, SelectList } from "@gajae-code/tui";
|
|
2
2
|
import { getSelectListTheme, theme } from "../../modes/theme/theme";
|
|
3
3
|
import { DynamicBorder } from "./dynamic-border";
|
|
4
|
+
import { DynamicThemeText } from "./dynamic-theme-text";
|
|
4
5
|
|
|
5
6
|
export interface FramedSelectOptions {
|
|
6
7
|
maxVisible?: number;
|
|
@@ -24,10 +25,10 @@ export function FramedSelect(
|
|
|
24
25
|
const container = new Container();
|
|
25
26
|
container.addChild(new DynamicBorder());
|
|
26
27
|
if (title) {
|
|
27
|
-
container.addChild(new
|
|
28
|
+
container.addChild(new DynamicThemeText(() => theme.bold(theme.fg("accent", title))));
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const selectList = new SelectList(items, opts.maxVisible ?? Math.min(items.length, 10), getSelectListTheme());
|
|
31
|
+
const selectList = new SelectList(items, opts.maxVisible ?? Math.min(items.length, 10), () => getSelectListTheme());
|
|
31
32
|
const selectedIndex =
|
|
32
33
|
opts.selectedValue === undefined ? -1 : items.findIndex(item => item.value === opts.selectedValue);
|
|
33
34
|
if (selectedIndex !== -1) selectList.setSelectedIndex(selectedIndex);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Component, Text } from "@gajae-code/tui";
|
|
2
|
+
|
|
3
|
+
/** Text whose styling is resolved at render time so live theme previews recolor open overlays. */
|
|
4
|
+
export class DynamicThemeText implements Component {
|
|
5
|
+
#text = new Text("", 0, 0);
|
|
6
|
+
#rendered = "";
|
|
7
|
+
|
|
8
|
+
constructor(private readonly getText: () => string) {}
|
|
9
|
+
|
|
10
|
+
invalidate(): void {
|
|
11
|
+
this.#text.invalidate();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
render(width: number): string[] {
|
|
15
|
+
const rendered = this.getText();
|
|
16
|
+
if (rendered !== this.#rendered) {
|
|
17
|
+
this.#rendered = rendered;
|
|
18
|
+
this.#text.setText(rendered);
|
|
19
|
+
}
|
|
20
|
+
return this.#text.render(width);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -3,16 +3,17 @@ import {
|
|
|
3
3
|
buildGajaePixelFrames,
|
|
4
4
|
type Component,
|
|
5
5
|
type Container,
|
|
6
|
-
type GajaePixelFrameName,
|
|
7
6
|
type GajaePixelFrames,
|
|
8
7
|
getCellDimensions,
|
|
9
|
-
PARA_PARA_STEPS,
|
|
10
8
|
PET_SKINS,
|
|
9
|
+
type PetBurst,
|
|
10
|
+
type PetFrameName,
|
|
11
11
|
type PetMode,
|
|
12
12
|
type PetSkinId,
|
|
13
13
|
petBurstDurationMs,
|
|
14
14
|
petBurstFrame,
|
|
15
15
|
registerAnimationCallback,
|
|
16
|
+
resolvePetMode,
|
|
16
17
|
type TUI,
|
|
17
18
|
} from "@gajae-code/tui";
|
|
18
19
|
import type { CustomEditor } from "./custom-editor";
|
|
@@ -66,21 +67,48 @@ function sameFootprint(left: SixelFootprint, right: SixelFootprint): boolean {
|
|
|
66
67
|
*/
|
|
67
68
|
const petOverlayEmitterOwners = new WeakMap<TUI, GajaePetWidget>();
|
|
68
69
|
|
|
69
|
-
/** Working animation: the shared para-para beats looped end to end. */
|
|
70
|
-
const WORK_LOOP_TOTAL = PARA_PARA_STEPS.reduce((sum, [, ms]) => sum + ms, 0);
|
|
71
70
|
/** Random gap between automatic claw flexes (fires while idle AND working). */
|
|
72
71
|
const AUTO_FLEX_MIN_GAP_MS = 12_000;
|
|
73
72
|
const AUTO_FLEX_MAX_GAP_MS = 40_000;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
[
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
[
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
73
|
+
|
|
74
|
+
function animationFrameAt(
|
|
75
|
+
steps: ReadonlyArray<readonly [PetFrameName, number]>,
|
|
76
|
+
now: number,
|
|
77
|
+
fallback: PetFrameName,
|
|
78
|
+
): PetFrameName {
|
|
79
|
+
const total = steps.reduce((sum, [, ms]) => sum + ms, 0);
|
|
80
|
+
if (total <= 0) return fallback;
|
|
81
|
+
let elapsed = now % total;
|
|
82
|
+
for (const [frame, ms] of steps) {
|
|
83
|
+
if (elapsed < ms) return frame;
|
|
84
|
+
elapsed -= ms;
|
|
85
|
+
}
|
|
86
|
+
return fallback;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function animationFrameAtElapsed(
|
|
90
|
+
steps: ReadonlyArray<readonly [PetFrameName, number]>,
|
|
91
|
+
elapsed: number,
|
|
92
|
+
fallback: PetFrameName,
|
|
93
|
+
): PetFrameName {
|
|
94
|
+
for (const [frame, ms] of steps) {
|
|
95
|
+
if (elapsed < ms) return frame;
|
|
96
|
+
elapsed -= ms;
|
|
97
|
+
}
|
|
98
|
+
return fallback;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function animationDuration(steps: ReadonlyArray<readonly [PetFrameName, number]>): number {
|
|
102
|
+
return steps.reduce((sum, [, ms]) => sum + ms, 0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface WorkTransition {
|
|
106
|
+
startedAt: number;
|
|
107
|
+
steps: ReadonlyArray<readonly [PetFrameName, number]>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const OUROBOROS_WORK_BURST_MIN_GAP_MS = 14_000;
|
|
111
|
+
const OUROBOROS_WORK_BURST_MAX_GAP_MS = 30_000;
|
|
84
112
|
|
|
85
113
|
/**
|
|
86
114
|
* Selector preview: fire the first burst this soon after a skin is previewed, so the
|
|
@@ -123,8 +151,8 @@ export class PetFramedEditor implements Component {
|
|
|
123
151
|
}
|
|
124
152
|
|
|
125
153
|
/**
|
|
126
|
-
* The gajae pet: a
|
|
127
|
-
*
|
|
154
|
+
* The gajae pet: a real-pixel sprite living in a reserved area beside the
|
|
155
|
+
* composer. It is nearest-neighbor scaled to the two terminal rows occupied
|
|
128
156
|
* by an empty one-line composer and lifted one row so its feet meet the input
|
|
129
157
|
* box's bottom edge.
|
|
130
158
|
*
|
|
@@ -146,12 +174,17 @@ export class GajaePetWidget {
|
|
|
146
174
|
#getComposerBottomOffset: () => number;
|
|
147
175
|
#mode: PetMode = "off";
|
|
148
176
|
#pixel: GajaePixelFrames | undefined;
|
|
149
|
-
#frame:
|
|
177
|
+
#frame: PetFrameName = "base";
|
|
150
178
|
#animation: AnimationRegistration | undefined;
|
|
151
179
|
#flexUntil = 0;
|
|
180
|
+
#activeBurst: PetBurst | undefined;
|
|
152
181
|
#nextAutoFlexAt = 0;
|
|
182
|
+
#working = false;
|
|
183
|
+
#workTransition: WorkTransition | undefined;
|
|
184
|
+
#nextWorkBurstAt = 0;
|
|
185
|
+
#workBurstIndex = 0;
|
|
153
186
|
#autoFlexGapMs: [number, number] | null;
|
|
154
|
-
#forcedProtocol: "sixel" | "kitty" | undefined;
|
|
187
|
+
#forcedProtocol: "sixel" | "kitty" | "iterm2" | undefined;
|
|
155
188
|
/** Cell metrics the current frames were built for; a change triggers a rebuild. */
|
|
156
189
|
#builtCellW = 0;
|
|
157
190
|
#builtCellH = 0;
|
|
@@ -178,7 +211,7 @@ export class GajaePetWidget {
|
|
|
178
211
|
isWorking: () => boolean;
|
|
179
212
|
/** Rows rendered below the composer box (pet floor + hook widgets). */
|
|
180
213
|
getComposerBottomOffset: () => number;
|
|
181
|
-
forcePixelProtocol?: "sixel" | "kitty";
|
|
214
|
+
forcePixelProtocol?: "sixel" | "kitty" | "iterm2";
|
|
182
215
|
/** Random [min, max] ms between auto-flexes; null disables. */
|
|
183
216
|
autoFlexGapMs?: [number, number] | null;
|
|
184
217
|
}) {
|
|
@@ -195,7 +228,7 @@ export class GajaePetWidget {
|
|
|
195
228
|
}
|
|
196
229
|
|
|
197
230
|
/** Protocol available for the real-pixel pet, if any. */
|
|
198
|
-
static pixelProtocol(): "sixel" | "kitty" | null {
|
|
231
|
+
static pixelProtocol(): "sixel" | "kitty" | "iterm2" | null {
|
|
199
232
|
return getPetPixelProtocol();
|
|
200
233
|
}
|
|
201
234
|
|
|
@@ -207,8 +240,8 @@ export class GajaePetWidget {
|
|
|
207
240
|
return performance.now() < this.#flexUntil;
|
|
208
241
|
}
|
|
209
242
|
|
|
210
|
-
setMode(mode:
|
|
211
|
-
this.#applyMode(mode, true);
|
|
243
|
+
setMode(mode: string): void {
|
|
244
|
+
this.#applyMode(resolvePetMode(mode), true);
|
|
212
245
|
}
|
|
213
246
|
|
|
214
247
|
/** Live preview during a selector: change the sprite without re-mounting the
|
|
@@ -237,6 +270,9 @@ export class GajaePetWidget {
|
|
|
237
270
|
this.#releaseOverlayEmitter();
|
|
238
271
|
this.#floorContainer.clear();
|
|
239
272
|
this.#pixel = undefined;
|
|
273
|
+
this.#workTransition = undefined;
|
|
274
|
+
this.#nextWorkBurstAt = 0;
|
|
275
|
+
this.#activeBurst = undefined;
|
|
240
276
|
this.#framedEditor.setReserve(0);
|
|
241
277
|
if (mountComposer) this.#mountEditor(false);
|
|
242
278
|
this.#ui.requestRender(true);
|
|
@@ -247,9 +283,14 @@ export class GajaePetWidget {
|
|
|
247
283
|
if (!protocol) return;
|
|
248
284
|
if (this.#mode !== "off") this.#writeImageCleanup();
|
|
249
285
|
this.#mode = mode;
|
|
250
|
-
this.#frame =
|
|
286
|
+
this.#frame = PET_SKINS[mode].baseFrame;
|
|
251
287
|
this.#flexUntil = 0;
|
|
288
|
+
this.#activeBurst = undefined;
|
|
252
289
|
this.#nextAutoFlexAt = 0;
|
|
290
|
+
this.#working = this.#isWorking();
|
|
291
|
+
this.#workTransition = undefined;
|
|
292
|
+
this.#nextWorkBurstAt = 0;
|
|
293
|
+
this.#workBurstIndex = 0;
|
|
253
294
|
this.#buildPixel(protocol);
|
|
254
295
|
if (mountComposer) this.#mountEditor(true);
|
|
255
296
|
// The pet overlays the composer's bottom rows; no floor row is reserved, so
|
|
@@ -262,7 +303,7 @@ export class GajaePetWidget {
|
|
|
262
303
|
}
|
|
263
304
|
|
|
264
305
|
/** (Re)build the encoded frames for the current terminal cell metrics. */
|
|
265
|
-
#buildPixel(protocol: "sixel" | "kitty"): void {
|
|
306
|
+
#buildPixel(protocol: "sixel" | "kitty" | "iterm2"): void {
|
|
266
307
|
const cell = getCellDimensions();
|
|
267
308
|
this.#builtCellW = cell.widthPx;
|
|
268
309
|
this.#builtCellH = cell.heightPx;
|
|
@@ -276,10 +317,17 @@ export class GajaePetWidget {
|
|
|
276
317
|
skin,
|
|
277
318
|
cellWidthPx: cell.widthPx,
|
|
278
319
|
cellHeightPx: cell.heightPx,
|
|
320
|
+
// Encode iTerm2 at the same two-row pixel size as Kitty/Sixel. The
|
|
321
|
+
// iTerm2 protocol receives explicit pixel dimensions below, so it must
|
|
322
|
+
// not be reduced to a fractional cell canvas or it becomes undersized.
|
|
279
323
|
targetRows: 2,
|
|
280
324
|
sixelTopPaddingPx: protocol === "sixel" ? PET_SIXEL_DROP_PX : 0,
|
|
281
325
|
kittyCellYOffsetPx: protocol === "kitty" ? petKittyDropPx(cell.heightPx) : 0,
|
|
282
326
|
kittyImageId: protocol === "kitty" ? this.#kittyImageId : undefined,
|
|
327
|
+
// iTerm2 receives an explicit 1:1 pixel size; keep its raster inside the
|
|
328
|
+
// same two-row footprint reserved for Kitty and Sixel.
|
|
329
|
+
iterm2TopPaddingPx: 0,
|
|
330
|
+
iterm2BottomPaddingPx: 0,
|
|
283
331
|
});
|
|
284
332
|
this.#framedEditor.setReserve(this.#pixel.columns + PET_SIDE_MARGIN);
|
|
285
333
|
}
|
|
@@ -306,6 +354,9 @@ export class GajaePetWidget {
|
|
|
306
354
|
this.#releaseOverlayEmitter();
|
|
307
355
|
this.#mode = "off";
|
|
308
356
|
this.#pixel = undefined;
|
|
357
|
+
this.#workTransition = undefined;
|
|
358
|
+
this.#nextWorkBurstAt = 0;
|
|
359
|
+
this.#activeBurst = undefined;
|
|
309
360
|
this.#floorContainer.clear();
|
|
310
361
|
this.#framedEditor.setReserve(0);
|
|
311
362
|
// Restore the plain composer only while our framed wrapper is still
|
|
@@ -334,31 +385,46 @@ export class GajaePetWidget {
|
|
|
334
385
|
this.#mountEditor(this.#mode !== "off");
|
|
335
386
|
}
|
|
336
387
|
|
|
337
|
-
#pickFrame(now: number):
|
|
388
|
+
#pickFrame(now: number): PetFrameName {
|
|
338
389
|
const mode = this.#mode;
|
|
339
390
|
if (mode === "off") return "base";
|
|
391
|
+
const skin = PET_SKINS[mode];
|
|
392
|
+
const working = this.#isWorking();
|
|
393
|
+
if (working !== this.#working) {
|
|
394
|
+
this.#working = working;
|
|
395
|
+
this.#flexUntil = 0;
|
|
396
|
+
this.#activeBurst = undefined;
|
|
397
|
+
this.#nextWorkBurstAt = 0;
|
|
398
|
+
const steps = working ? skin.workEnter : skin.workExit;
|
|
399
|
+
this.#workTransition = steps?.length ? { startedAt: now, steps } : undefined;
|
|
400
|
+
}
|
|
401
|
+
if (this.#workTransition) {
|
|
402
|
+
const elapsed = now - this.#workTransition.startedAt;
|
|
403
|
+
const duration = animationDuration(this.#workTransition.steps);
|
|
404
|
+
if (elapsed < duration) {
|
|
405
|
+
return animationFrameAtElapsed(this.#workTransition.steps, elapsed, skin.baseFrame);
|
|
406
|
+
}
|
|
407
|
+
this.#workTransition = undefined;
|
|
408
|
+
}
|
|
340
409
|
// Random idle burst → the skin's own animation, driven by its burst descriptor
|
|
341
410
|
// (RedGajae holds a flex; BlueGajae dances the para-para then sobs).
|
|
342
411
|
if (now < this.#flexUntil) {
|
|
343
|
-
const burst =
|
|
412
|
+
const burst = this.#activeBurst ?? skin.burst;
|
|
344
413
|
const elapsed = now - (this.#flexUntil - petBurstDurationMs(burst));
|
|
345
414
|
return petBurstFrame(burst, elapsed, now);
|
|
346
415
|
}
|
|
347
|
-
// Working → loop the
|
|
416
|
+
// Working → loop the selected skin's activity animation.
|
|
348
417
|
if (this.#isWorking()) {
|
|
349
|
-
|
|
350
|
-
for (const [frame, ms] of PARA_PARA_STEPS) {
|
|
351
|
-
if (d < ms) return frame;
|
|
352
|
-
d -= ms;
|
|
353
|
-
}
|
|
354
|
-
return "base";
|
|
418
|
+
return animationFrameAt(skin.work, now, skin.baseFrame);
|
|
355
419
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
420
|
+
return animationFrameAt(skin.idle, now, skin.baseFrame);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#scheduleWorkBurst(now: number): void {
|
|
424
|
+
this.#nextWorkBurstAt =
|
|
425
|
+
now +
|
|
426
|
+
OUROBOROS_WORK_BURST_MIN_GAP_MS +
|
|
427
|
+
Math.random() * (OUROBOROS_WORK_BURST_MAX_GAP_MS - OUROBOROS_WORK_BURST_MIN_GAP_MS);
|
|
362
428
|
}
|
|
363
429
|
|
|
364
430
|
#scheduleAutoFlex(now: number): void {
|
|
@@ -382,15 +448,29 @@ export class GajaePetWidget {
|
|
|
382
448
|
}
|
|
383
449
|
// Random show-off, both while idle and while working. Each skin's burst runs for
|
|
384
450
|
// its own length (RedGajae a brief flex; BlueGajae a para-para cycle plus sob).
|
|
385
|
-
|
|
451
|
+
const skin = PET_SKINS[this.#mode];
|
|
452
|
+
const usesWorkBursts = this.#isWorking() && Boolean(skin.workBursts?.length);
|
|
453
|
+
if (this.#autoFlexGapMs && !usesWorkBursts && now >= this.#flexUntil) {
|
|
386
454
|
if (this.#nextAutoFlexAt === 0) {
|
|
387
455
|
this.#scheduleAutoFlex(now);
|
|
388
456
|
} else if (now >= this.#nextAutoFlexAt) {
|
|
389
457
|
const burstMs = petBurstDurationMs(PET_SKINS[this.#mode].burst);
|
|
458
|
+
this.#activeBurst = skin.burst;
|
|
390
459
|
this.#flexUntil = now + burstMs;
|
|
391
460
|
this.#scheduleAutoFlex(now + burstMs);
|
|
392
461
|
}
|
|
393
462
|
}
|
|
463
|
+
if (this.#isWorking() && !this.#workTransition && now >= this.#flexUntil && skin.workBursts?.length) {
|
|
464
|
+
if (this.#nextWorkBurstAt === 0) {
|
|
465
|
+
this.#scheduleWorkBurst(now);
|
|
466
|
+
} else if (now >= this.#nextWorkBurstAt) {
|
|
467
|
+
const burst = skin.workBursts[this.#workBurstIndex % skin.workBursts.length];
|
|
468
|
+
this.#workBurstIndex++;
|
|
469
|
+
this.#activeBurst = burst;
|
|
470
|
+
this.#flexUntil = now + petBurstDurationMs(burst);
|
|
471
|
+
this.#scheduleWorkBurst(this.#flexUntil);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
394
474
|
const frame = this.#pickFrame(now);
|
|
395
475
|
if (frame === this.#frame) return;
|
|
396
476
|
this.#frame = frame;
|
|
@@ -412,7 +492,15 @@ export class GajaePetWidget {
|
|
|
412
492
|
// back onto the composer's bottom border per protocol (sixel via transparent
|
|
413
493
|
// top padding, kitty via a sub-cell Y offset baked into the frames).
|
|
414
494
|
const composerBottom = rows - this.#getComposerBottomOffset();
|
|
415
|
-
|
|
495
|
+
// iTerm2 anchors an inline image at the current cursor row. Keep the full
|
|
496
|
+
// sprite inside the composer rows and leave the HUD boundary below it.
|
|
497
|
+
// Keep the full-size iTerm2 sprite aligned with the composer like the
|
|
498
|
+
// cursor-neutral Kitty/Sixel overlays; only the protocol encoding differs.
|
|
499
|
+
// Nudge the iTerm2 image one terminal row upward while preserving its
|
|
500
|
+
// explicit 36px size and aspect ratio. Kitty/Sixel keep their established
|
|
501
|
+
// safety lift.
|
|
502
|
+
const safetyLift = PET_RAISE_ROWS;
|
|
503
|
+
const y = composerBottom - pixel.rows - safetyLift;
|
|
416
504
|
const x = columns - pixel.columns - PET_SIDE_MARGIN;
|
|
417
505
|
if (y < 0 || x < 0) return null;
|
|
418
506
|
return { x, y };
|
|
@@ -490,7 +578,7 @@ export class GajaePetWidget {
|
|
|
490
578
|
const { x, y } = pos;
|
|
491
579
|
let out = "";
|
|
492
580
|
|
|
493
|
-
if (pixel.protocol === "sixel") {
|
|
581
|
+
if (pixel.protocol === "sixel" || pixel.protocol === "iterm2") {
|
|
494
582
|
const footprint = { x, y, columns: pixel.columns, rows: pixel.rasterRows };
|
|
495
583
|
if (this.#lastSixelFootprint && !sameFootprint(this.#lastSixelFootprint, footprint)) {
|
|
496
584
|
out += this.#clearSixelFootprint(this.#lastSixelFootprint);
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
GJC_MODEL_ASSIGNMENT_TARGETS,
|
|
28
28
|
isAuthenticated,
|
|
29
29
|
kNoAuth,
|
|
30
|
+
requiresExplicitThinkingChoice,
|
|
30
31
|
} from "../../config/model-registry";
|
|
31
32
|
import {
|
|
32
33
|
formatModelSelectorValue,
|
|
@@ -2432,12 +2433,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
2432
2433
|
}
|
|
2433
2434
|
}
|
|
2434
2435
|
|
|
2435
|
-
function requiresExplicitThinkingChoice(model: Model, role: GjcModelAssignmentTargetId | null): boolean {
|
|
2436
|
-
if (model.reasoning !== true) return false;
|
|
2437
|
-
if (model.provider === "openai" || model.provider === "openai-codex") return true;
|
|
2438
|
-
return role !== null && GJC_MODEL_ASSIGNMENT_TARGETS[role].settingsPath === "task.agentModelOverrides";
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
2436
|
function getSelectableThinkingLevels(model: Model): ThinkingLevel[] {
|
|
2442
2437
|
const levels: ThinkingLevel[] = [ThinkingLevel.Off];
|
|
2443
2438
|
let efforts: readonly string[];
|
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
TERMINAL,
|
|
8
8
|
} from "@gajae-code/tui";
|
|
9
9
|
|
|
10
|
-
export type PetPixelProtocol = "sixel" | "kitty";
|
|
10
|
+
export type PetPixelProtocol = "sixel" | "kitty" | "iterm2";
|
|
11
11
|
|
|
12
|
-
export const PET_UNAVAILABLE_DESCRIPTION = "Unavailable: requires compatible Kitty or Sixel overlay rendering";
|
|
12
|
+
export const PET_UNAVAILABLE_DESCRIPTION = "Unavailable: requires compatible Kitty, iTerm2, or Sixel overlay rendering";
|
|
13
13
|
export const PET_SAVED_UNAVAILABLE_DESCRIPTION =
|
|
14
|
-
"Saved, unavailable — requires compatible Kitty or Sixel overlay rendering";
|
|
14
|
+
"Saved, unavailable — requires compatible Kitty, iTerm2, or Sixel overlay rendering";
|
|
15
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.";
|
|
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, iTerm2, or a terminal with compatible Sixel support.";
|
|
17
17
|
const PET_MULTIPLEXER_UNAVAILABLE_WARNING =
|
|
18
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
19
|
|
|
@@ -21,9 +21,14 @@ export function getPetUnavailableWarning(env: NodeJS.ProcessEnv = Bun.env): stri
|
|
|
21
21
|
return isUnderTerminalMultiplexer(env) ? PET_MULTIPLEXER_UNAVAILABLE_WARNING : PET_UNAVAILABLE_WARNING;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function getPetPixelProtocol(): PetPixelProtocol | null {
|
|
24
|
+
export function getPetPixelProtocol(env: NodeJS.ProcessEnv = Bun.env): PetPixelProtocol | null {
|
|
25
|
+
if (isUnderTerminalMultiplexer(env)) {
|
|
26
|
+
const forced = env.PI_FORCE_IMAGE_PROTOCOL?.trim().toLowerCase();
|
|
27
|
+
return TERMINAL.imageProtocol === ImageProtocol.Sixel && forced === "sixel" ? "sixel" : null;
|
|
28
|
+
}
|
|
25
29
|
if (TERMINAL.imageProtocol === ImageProtocol.Kitty) return "kitty";
|
|
26
30
|
if (TERMINAL.imageProtocol === ImageProtocol.Sixel) return "sixel";
|
|
31
|
+
if (TERMINAL.imageProtocol === ImageProtocol.Iterm2) return "iterm2";
|
|
27
32
|
return null;
|
|
28
33
|
}
|
|
29
34
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Container,
|
|
6
6
|
Input,
|
|
7
7
|
matchesKey,
|
|
8
|
+
resolvePetMode,
|
|
8
9
|
type SelectItem,
|
|
9
10
|
SelectList,
|
|
10
11
|
type SettingItem,
|
|
@@ -27,6 +28,7 @@ import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme }
|
|
|
27
28
|
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
|
28
29
|
import { getTabBarTheme } from "../shared";
|
|
29
30
|
import { DynamicBorder } from "./dynamic-border";
|
|
31
|
+
import { DynamicThemeText } from "./dynamic-theme-text";
|
|
30
32
|
import { GjcBundleSettingsComponent } from "./gjc-bundle-settings";
|
|
31
33
|
import {
|
|
32
34
|
type NotificationsEditorOperations,
|
|
@@ -101,18 +103,18 @@ class SelectSubmenu extends Container {
|
|
|
101
103
|
super();
|
|
102
104
|
|
|
103
105
|
// Title
|
|
104
|
-
this.addChild(new
|
|
106
|
+
this.addChild(new DynamicThemeText(() => theme.bold(theme.fg("accent", title))));
|
|
105
107
|
|
|
106
108
|
// Description
|
|
107
109
|
if (description) {
|
|
108
110
|
this.addChild(new Spacer(1));
|
|
109
|
-
this.addChild(new
|
|
111
|
+
this.addChild(new DynamicThemeText(() => theme.fg("muted", description)));
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
// Preview (if provided)
|
|
113
115
|
if (getPreview) {
|
|
114
116
|
this.addChild(new Spacer(1));
|
|
115
|
-
this.addChild(new
|
|
117
|
+
this.addChild(new DynamicThemeText(() => theme.fg("muted", "Preview:")));
|
|
116
118
|
this.#previewText = new Text(getPreview(), 0, 0);
|
|
117
119
|
this.addChild(this.#previewText);
|
|
118
120
|
}
|
|
@@ -121,7 +123,7 @@ class SelectSubmenu extends Container {
|
|
|
121
123
|
this.addChild(new Spacer(1));
|
|
122
124
|
|
|
123
125
|
// Select list
|
|
124
|
-
this.#selectList = new SelectList(options, Math.min(options.length, 10), getSelectListTheme());
|
|
126
|
+
this.#selectList = new SelectList(options, Math.min(options.length, 10), () => getSelectListTheme());
|
|
125
127
|
|
|
126
128
|
// Pre-select current value
|
|
127
129
|
const currentIndex = options.findIndex(o => o.value === currentValue);
|
|
@@ -157,7 +159,7 @@ class SelectSubmenu extends Container {
|
|
|
157
159
|
|
|
158
160
|
// Hint
|
|
159
161
|
this.addChild(new Spacer(1));
|
|
160
|
-
this.addChild(new
|
|
162
|
+
this.addChild(new DynamicThemeText(() => theme.fg("dim", " Enter to select · Esc to go back")));
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
#updatePreview(): void {
|
|
@@ -708,6 +710,11 @@ export interface SettingsCallbacks {
|
|
|
708
710
|
onThemePreview?: (theme: string) => void | Promise<void>;
|
|
709
711
|
/** Called to restore the rendered theme when theme settings preview is cancelled */
|
|
710
712
|
onThemePreviewCancel?: (theme: string) => void | Promise<void>;
|
|
713
|
+
/**
|
|
714
|
+
* Atomically apply and persist a theme selection. Returns false when the
|
|
715
|
+
* candidate could not be loaded, leaving the submenu open.
|
|
716
|
+
*/
|
|
717
|
+
onThemeCommit?: (path: "theme.dark" | "theme.light", theme: string, previousTheme: string) => Promise<boolean>;
|
|
711
718
|
/** Called to live-preview the gajae pet skin while browsing the pet setting. */
|
|
712
719
|
onPetPreview?: (mode: string) => void;
|
|
713
720
|
/**
|
|
@@ -971,6 +978,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
971
978
|
}
|
|
972
979
|
let description = def.description;
|
|
973
980
|
if (def.path === "pet.mode") {
|
|
981
|
+
currentValue = resolvePetMode(currentValue);
|
|
974
982
|
const petAvailable = this.context.petAvailable ?? isPetAvailable();
|
|
975
983
|
options = createPetSelectItems(options, currentValue, petAvailable);
|
|
976
984
|
// Unsupported terminals must see the same actionable guidance the
|
|
@@ -1072,6 +1080,13 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1072
1080
|
done(value);
|
|
1073
1081
|
return;
|
|
1074
1082
|
}
|
|
1083
|
+
if (def.path === "theme.dark" || def.path === "theme.light") {
|
|
1084
|
+
if (!this.callbacks.onThemeCommit) return;
|
|
1085
|
+
void this.callbacks.onThemeCommit(def.path, value, currentValue).then(accepted => {
|
|
1086
|
+
if (accepted) done(value);
|
|
1087
|
+
});
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1075
1090
|
if (def.path === "pet.mode") {
|
|
1076
1091
|
// The shared pet commit policy rechecks capability immediately
|
|
1077
1092
|
// before mutation and persists only on acceptance; the settings
|
|
@@ -32,7 +32,7 @@ function normalizePremiumRequests(value: number): number {
|
|
|
32
32
|
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
function scratchRoots(): readonly string[] {
|
|
36
36
|
const roots = new Set<string>([os.tmpdir(), path.join(os.homedir(), "tmp")]);
|
|
37
37
|
if (process.platform === "win32") {
|
|
38
38
|
const { TEMP, TMP, SystemRoot } = process.env;
|
|
@@ -48,10 +48,10 @@ const SCRATCH_ROOTS: readonly string[] = (() => {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
return [...roots];
|
|
51
|
-
}
|
|
51
|
+
}
|
|
52
52
|
|
|
53
53
|
function classifyProjectDir(pwd: string): { scratch: boolean; relative: string | null } {
|
|
54
|
-
for (const root of
|
|
54
|
+
for (const root of scratchRoots()) {
|
|
55
55
|
if (pathIsWithin(root, pwd)) {
|
|
56
56
|
return { scratch: true, relative: relativePathWithinRoot(root, pwd) };
|
|
57
57
|
}
|
|
@@ -4,7 +4,7 @@ import type { Api, Model } from "@gajae-code/ai/core";
|
|
|
4
4
|
import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
|
|
5
5
|
import type { OAuthProvider } from "@gajae-code/ai/utils/oauth/types";
|
|
6
6
|
import type { Component, OverlayHandle, SlashCommand } from "@gajae-code/tui";
|
|
7
|
-
import { Input,
|
|
7
|
+
import { Input, Loader, resolvePetMode, Spacer, Text } from "@gajae-code/tui";
|
|
8
8
|
import { getAgentDbPath, getProjectDir, logger, VERSION } from "@gajae-code/utils";
|
|
9
9
|
import type { AppKeybinding } from "../../config/keybindings";
|
|
10
10
|
import {
|
|
@@ -1772,6 +1772,36 @@ export class SelectorController {
|
|
|
1772
1772
|
this.#refreshThemeUi();
|
|
1773
1773
|
});
|
|
1774
1774
|
},
|
|
1775
|
+
onThemeCommit: async (path, themeName, previousTheme) => {
|
|
1776
|
+
if (!settings.canWriteDurableConfig()) {
|
|
1777
|
+
this.ctx.showError(
|
|
1778
|
+
"Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
|
|
1779
|
+
);
|
|
1780
|
+
return false;
|
|
1781
|
+
}
|
|
1782
|
+
const applied = await restoreThemePreview(themeName);
|
|
1783
|
+
if (!applied.success) {
|
|
1784
|
+
if (applied.error && !isThemePreviewSuperseded(applied)) {
|
|
1785
|
+
this.ctx.showError(`Failed to apply theme "${themeName}": ${applied.error}`);
|
|
1786
|
+
}
|
|
1787
|
+
const restored = await restoreThemePreview(previousTheme);
|
|
1788
|
+
if (!restored.success && restored.error && !isThemePreviewSuperseded(restored)) {
|
|
1789
|
+
this.ctx.showError(`Failed to restore theme preview: ${restored.error}`);
|
|
1790
|
+
}
|
|
1791
|
+
this.#refreshThemeUi();
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
try {
|
|
1795
|
+
settings.set(path, themeName);
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
await restoreThemePreview(previousTheme);
|
|
1798
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
1799
|
+
this.#refreshThemeUi();
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
this.#refreshThemeUi();
|
|
1803
|
+
return true;
|
|
1804
|
+
},
|
|
1775
1805
|
onPetPreview: mode => {
|
|
1776
1806
|
this.ctx.previewPetMode(mode as PetMode);
|
|
1777
1807
|
},
|
|
@@ -1853,6 +1883,8 @@ export class SelectorController {
|
|
|
1853
1883
|
showThemeSelector(): void {
|
|
1854
1884
|
getAvailableThemes().then(availableThemes => {
|
|
1855
1885
|
const initialTheme = getCurrentThemeName() ?? "red-claw";
|
|
1886
|
+
const settingsPath = getDetectedThemeSettingsPath();
|
|
1887
|
+
const savedTheme = settings.get(settingsPath);
|
|
1856
1888
|
this.showSelector(done => {
|
|
1857
1889
|
const restoreAndClose = () => {
|
|
1858
1890
|
void restoreThemePreview(initialTheme).then(result => {
|
|
@@ -1875,7 +1907,7 @@ export class SelectorController {
|
|
|
1875
1907
|
return;
|
|
1876
1908
|
}
|
|
1877
1909
|
try {
|
|
1878
|
-
settings.set(
|
|
1910
|
+
settings.set(settingsPath, themeName);
|
|
1879
1911
|
} catch (error) {
|
|
1880
1912
|
if (!settings.canWriteDurableConfig()) {
|
|
1881
1913
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
@@ -1884,8 +1916,21 @@ export class SelectorController {
|
|
|
1884
1916
|
}
|
|
1885
1917
|
throw error;
|
|
1886
1918
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1919
|
+
void restoreThemePreview(themeName).then(result => {
|
|
1920
|
+
if (!result.success && result.error && !isThemePreviewSuperseded(result)) {
|
|
1921
|
+
this.ctx.showError(`Failed to apply theme: ${result.error}`);
|
|
1922
|
+
settings.set(settingsPath, savedTheme);
|
|
1923
|
+
void restoreThemePreview(initialTheme).then(recovery => {
|
|
1924
|
+
if (!recovery.success && recovery.error && !isThemePreviewSuperseded(recovery)) {
|
|
1925
|
+
this.ctx.showError(`Failed to restore theme preview: ${recovery.error}`);
|
|
1926
|
+
}
|
|
1927
|
+
this.#refreshThemeUi();
|
|
1928
|
+
});
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
this.#refreshThemeUi();
|
|
1932
|
+
done();
|
|
1933
|
+
});
|
|
1889
1934
|
},
|
|
1890
1935
|
restoreAndClose,
|
|
1891
1936
|
themeName => {
|
|
@@ -1904,7 +1949,7 @@ export class SelectorController {
|
|
|
1904
1949
|
|
|
1905
1950
|
showPetSelector(): void {
|
|
1906
1951
|
const stored = settings.get("pet.mode");
|
|
1907
|
-
const initial: PetMode =
|
|
1952
|
+
const initial: PetMode = resolvePetMode(stored);
|
|
1908
1953
|
this.showSelector(done => {
|
|
1909
1954
|
// Live-preview via previewMode (no editor re-mount, so the overlay stays);
|
|
1910
1955
|
// Enter commits + persists, Esc restores the initial skin.
|
|
@@ -2091,6 +2136,17 @@ export class SelectorController {
|
|
|
2091
2136
|
});
|
|
2092
2137
|
break;
|
|
2093
2138
|
}
|
|
2139
|
+
case "theme.dark":
|
|
2140
|
+
case "theme.light": {
|
|
2141
|
+
restoreThemePreview(value as string).then(result => {
|
|
2142
|
+
if (this.ctx.isStopped?.()) return;
|
|
2143
|
+
this.#refreshThemeUi();
|
|
2144
|
+
if (!result.success && result.error && !isThemePreviewSuperseded(result)) {
|
|
2145
|
+
this.ctx.showError(`Failed to apply theme "${value}": ${result.error}`);
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
break;
|
|
2149
|
+
}
|
|
2094
2150
|
case "pet.mode":
|
|
2095
2151
|
// The settings submenu already persisted the value; apply it to the live
|
|
2096
2152
|
// widget via previewMode (the settings overlay is still open, so a full
|
|
@@ -1345,7 +1345,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1345
1345
|
this.editorContainer.clear();
|
|
1346
1346
|
this.editorContainer.addChild(this.editor);
|
|
1347
1347
|
}
|
|
1348
|
+
// Re-mounting after a cancelled action must invalidate the editor so the
|
|
1349
|
+
// inline prompt suggestion is rendered again on the restored composer.
|
|
1350
|
+
this.editor.invalidate();
|
|
1348
1351
|
this.ui.setFocus(this.editor);
|
|
1352
|
+
this.ui.requestRender();
|
|
1349
1353
|
}
|
|
1350
1354
|
|
|
1351
1355
|
#createPetWidget(editor: CustomEditor): GajaePetWidget {
|