@gajae-code/coding-agent 0.7.7 → 0.7.9

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.
Files changed (96) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +14 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/update-cli.d.ts +24 -1
  6. package/dist/types/commands/local-provider.d.ts +34 -0
  7. package/dist/types/config/keybindings.d.ts +1 -1
  8. package/dist/types/config/model-profile-activation.d.ts +19 -2
  9. package/dist/types/config/model-profiles.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +9 -0
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +45 -0
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  15. package/dist/types/gjc-runtime/launch-tmux.d.ts +14 -5
  16. package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
  17. package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
  18. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +3 -1
  19. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  20. package/dist/types/modes/components/hook-input.d.ts +1 -0
  21. package/dist/types/modes/components/model-selector.d.ts +11 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +4 -1
  23. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  24. package/dist/types/modes/components/status-line.d.ts +7 -3
  25. package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
  26. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  27. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  28. package/dist/types/modes/interactive-mode.d.ts +3 -1
  29. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  30. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +21 -1
  31. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  32. package/dist/types/modes/types.d.ts +3 -1
  33. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  34. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  35. package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
  36. package/dist/types/notifications/telegram-daemon.d.ts +1 -10
  37. package/dist/types/tools/ask.d.ts +24 -0
  38. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  39. package/package.json +7 -7
  40. package/src/cli/local-provider-smoke.ts +632 -0
  41. package/src/cli/notify-cli.ts +5 -3
  42. package/src/cli/update-cli.ts +155 -13
  43. package/src/cli.ts +19 -0
  44. package/src/commands/launch.ts +46 -1
  45. package/src/commands/local-provider.ts +62 -0
  46. package/src/commands/notify.ts +1 -1
  47. package/src/config/keybindings.ts +1 -1
  48. package/src/config/model-profile-activation.ts +92 -11
  49. package/src/config/model-profiles.ts +15 -2
  50. package/src/config/model-registry.ts +136 -6
  51. package/src/config/models-config-schema.ts +9 -0
  52. package/src/config/settings-schema.ts +48 -0
  53. package/src/config/settings.ts +11 -0
  54. package/src/coordinator-mcp/server.ts +280 -36
  55. package/src/deep-interview/render-middleware.ts +1 -0
  56. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -6
  57. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
  58. package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
  59. package/src/gjc-runtime/launch-tmux.ts +321 -38
  60. package/src/gjc-runtime/launch-worktree.ts +16 -2
  61. package/src/gjc-runtime/team-runtime.ts +87 -7
  62. package/src/gjc-runtime/tmux-common.ts +16 -0
  63. package/src/gjc-runtime/tmux-sessions.ts +13 -9
  64. package/src/gjc-runtime/ultragoal-guard.ts +11 -5
  65. package/src/internal-urls/docs-index.generated.ts +10 -9
  66. package/src/modes/components/agent-dashboard.ts +2 -1
  67. package/src/modes/components/custom-model-preset-wizard.ts +31 -192
  68. package/src/modes/components/hook-editor.ts +7 -4
  69. package/src/modes/components/hook-input.ts +4 -0
  70. package/src/modes/components/model-selector.ts +260 -12
  71. package/src/modes/components/session-observer-overlay.ts +3 -3
  72. package/src/modes/components/settings-selector.ts +506 -5
  73. package/src/modes/components/status-line/segments.ts +16 -19
  74. package/src/modes/components/status-line/types.ts +6 -2
  75. package/src/modes/components/status-line.ts +70 -35
  76. package/src/modes/controllers/event-controller.ts +98 -4
  77. package/src/modes/controllers/extension-ui-controller.ts +5 -0
  78. package/src/modes/controllers/input-controller.ts +15 -0
  79. package/src/modes/controllers/selector-controller.ts +142 -11
  80. package/src/modes/interactive-mode.ts +7 -2
  81. package/src/modes/prompt-action-autocomplete.ts +8 -0
  82. package/src/modes/shared/agent-wire/deep-interview-gate.ts +11 -4
  83. package/src/modes/tmux-scroll.ts +92 -0
  84. package/src/modes/types.ts +6 -1
  85. package/src/modes/utils/terminal-bell.ts +48 -0
  86. package/src/notifications/daemon-paths.ts +22 -0
  87. package/src/notifications/telegram-daemon-cli.ts +125 -13
  88. package/src/notifications/telegram-daemon.ts +6 -21
  89. package/src/prompts/system/system-prompt.md +5 -0
  90. package/src/sdk.ts +10 -1
  91. package/src/session/agent-session.ts +84 -6
  92. package/src/slash-commands/builtin-registry.ts +106 -13
  93. package/src/tools/ask.ts +11 -1
  94. package/src/tools/eval.ts +2 -2
  95. package/src/tools/render-utils.ts +2 -0
  96. package/src/tools/ultragoal-ask-guard.ts +39 -5
@@ -27,7 +27,9 @@ import { getTabBarTheme } from "../shared";
27
27
  import { DynamicBorder } from "./dynamic-border";
28
28
  import { handleInputOrEscape, PluginSettingsComponent } from "./plugin-settings";
29
29
  import { getSettingsForTab, type SettingDef } from "./settings-defs";
30
+ import type { StatusLineSegmentOptions } from "./status-line";
30
31
  import { getPreset } from "./status-line/presets";
32
+ import { ALL_SEGMENT_IDS } from "./status-line/segments";
31
33
 
32
34
  /**
33
35
  * A submenu component for selecting from a list of options.
@@ -161,6 +163,457 @@ class SelectSubmenu extends Container {
161
163
  this.#selectList.handleInput(data);
162
164
  }
163
165
  }
166
+ const STATUS_LINE_CUSTOM_EDITOR_ID = "statusLine.customEditor";
167
+ const PUBLIC_STATUS_SEGMENTS = ALL_SEGMENT_IDS.filter(id => id !== "pi");
168
+
169
+ type StatusLineDraft = Required<
170
+ Pick<StatusLinePreviewSettings, "preset" | "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
171
+ >;
172
+
173
+ const BOOL_VALUES = ["true", "false"];
174
+ const PATH_LENGTH_OPTIONS: SelectItem[] = [16, 24, 32, 40, 50, 60, 80].map(value => ({
175
+ value: String(value),
176
+ label: String(value),
177
+ }));
178
+ const TIME_FORMAT_OPTIONS: SelectItem[] = [
179
+ { value: "24h", label: "24h" },
180
+ { value: "12h", label: "12h" },
181
+ ];
182
+ const USAGE_MODE_OPTIONS: SelectItem[] = [
183
+ { value: "used", label: "Used" },
184
+ { value: "remaining", label: "Remaining" },
185
+ ];
186
+
187
+ function cloneSegmentOptions(options: StatusLineSegmentOptions | undefined): StatusLineSegmentOptions {
188
+ return mergeSegmentOptions(undefined, options);
189
+ }
190
+
191
+ function mergeSegmentOptions(
192
+ base: StatusLineSegmentOptions | undefined,
193
+ overrides: StatusLineSegmentOptions | undefined,
194
+ ): StatusLineSegmentOptions {
195
+ return {
196
+ model: base?.model || overrides?.model ? { ...(base?.model ?? {}), ...(overrides?.model ?? {}) } : undefined,
197
+ path: base?.path || overrides?.path ? { ...(base?.path ?? {}), ...(overrides?.path ?? {}) } : undefined,
198
+ git: base?.git || overrides?.git ? { ...(base?.git ?? {}), ...(overrides?.git ?? {}) } : undefined,
199
+ time: base?.time || overrides?.time ? { ...(base?.time ?? {}), ...(overrides?.time ?? {}) } : undefined,
200
+ usage: base?.usage || overrides?.usage ? { ...(base?.usage ?? {}), ...(overrides?.usage ?? {}) } : undefined,
201
+ };
202
+ }
203
+
204
+ function effectiveSegmentOptions(
205
+ preset: StatusLinePreset,
206
+ options: StatusLineSegmentOptions | undefined,
207
+ ): StatusLineSegmentOptions {
208
+ return mergeSegmentOptions(getPreset(preset).segmentOptions, options);
209
+ }
210
+
211
+ function effectiveCustomSegments(
212
+ preset: StatusLinePreset,
213
+ leftSegments: StatusLineSegmentId[],
214
+ rightSegments: StatusLineSegmentId[],
215
+ ): { leftSegments: StatusLineSegmentId[]; rightSegments: StatusLineSegmentId[] } {
216
+ if (preset === "custom") {
217
+ return { leftSegments: [...leftSegments], rightSegments: [...rightSegments] };
218
+ }
219
+ const presetDef = getPreset(preset);
220
+ return {
221
+ leftSegments: [...presetDef.leftSegments],
222
+ rightSegments: [...presetDef.rightSegments],
223
+ };
224
+ }
225
+
226
+ function statusSegmentLabel(id: StatusLineSegmentId): string {
227
+ return id.replace(/_/g, " ");
228
+ }
229
+
230
+ function segmentPlacement(
231
+ id: StatusLineSegmentId,
232
+ leftSegments: StatusLineSegmentId[],
233
+ rightSegments: StatusLineSegmentId[],
234
+ ): "hidden" | "left" | "right" {
235
+ if (leftSegments.includes(id)) return "left";
236
+ if (rightSegments.includes(id)) return "right";
237
+ return "hidden";
238
+ }
239
+
240
+ class StatusLineCustomEditor extends Container {
241
+ #list!: SettingsList;
242
+ #draft: StatusLineDraft;
243
+ #previewHighlightSegment: StatusLineSegmentId | undefined;
244
+
245
+ constructor(
246
+ private readonly callbacks: SettingsCallbacks,
247
+ private readonly done: (value?: string) => void,
248
+ ) {
249
+ super();
250
+ const preset = settings.get("statusLine.preset");
251
+ const seeded = effectiveCustomSegments(
252
+ preset,
253
+ settings.get("statusLine.leftSegments"),
254
+ settings.get("statusLine.rightSegments"),
255
+ );
256
+ this.#draft = {
257
+ preset: "custom",
258
+ leftSegments: seeded.leftSegments,
259
+ rightSegments: seeded.rightSegments,
260
+ separator: settings.get("statusLine.separator"),
261
+ segmentOptions: effectiveSegmentOptions(
262
+ preset,
263
+ settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions,
264
+ ),
265
+ };
266
+ this.#preview();
267
+ this.#build();
268
+ }
269
+
270
+ #build(): void {
271
+ this.clear();
272
+ this.addChild(new Text(theme.bold(theme.fg("accent", "Status Line Custom Editor")), 0, 0));
273
+ this.addChild(new Spacer(1));
274
+ this.#list = new SettingsList(
275
+ this.#items(),
276
+ 14,
277
+ getSettingsListTheme(),
278
+ (id, value) => this.#handleChange(id, value),
279
+ () => this.#cancel(),
280
+ item => this.#setSelectedItem(item),
281
+ 2,
282
+ );
283
+ this.addChild(this.#list);
284
+ }
285
+
286
+ #refresh(): void {
287
+ this.#list.setItems(this.#items());
288
+ }
289
+ #setSelectedItem(item: SettingItem | undefined): void {
290
+ this.#previewHighlightSegment = this.#highlightSegmentForItem(item);
291
+ this.#preview();
292
+ }
293
+
294
+ #highlightSegmentForItem(item: SettingItem | undefined): StatusLineSegmentId | undefined {
295
+ if (!item) return undefined;
296
+ if (item.id.startsWith("segment.")) {
297
+ return item.id.slice("segment.".length) as StatusLineSegmentId;
298
+ }
299
+ if (item.id.startsWith("moveup.")) {
300
+ return item.id.slice("moveup.".length) as StatusLineSegmentId;
301
+ }
302
+ if (item.id.startsWith("movedown.")) {
303
+ return item.id.slice("movedown.".length) as StatusLineSegmentId;
304
+ }
305
+ if (item.id.startsWith("option.")) {
306
+ const [segment] = item.id.slice("option.".length).split(".");
307
+ return segment as StatusLineSegmentId;
308
+ }
309
+ return undefined;
310
+ }
311
+
312
+ #items(): SettingItem[] {
313
+ const items: SettingItem[] = [
314
+ {
315
+ id: "action.save",
316
+ label: "Save custom status line",
317
+ description: "Persist the previewed custom status line to user config.",
318
+ currentValue: "approve",
319
+ values: ["approve"],
320
+ },
321
+ {
322
+ id: "action.cancel",
323
+ label: "Cancel and restore",
324
+ description: "Close without persisting draft settings and restore the previous preview.",
325
+ currentValue: "restore",
326
+ values: ["restore"],
327
+ },
328
+ {
329
+ id: "separator",
330
+ label: "Separator",
331
+ description: "Separator style between status line segments.",
332
+ currentValue: this.#draft.separator,
333
+ submenu: (currentValue, done) =>
334
+ new SelectSubmenu(
335
+ "Status Line Separator",
336
+ "Style of separators between segments.",
337
+ [
338
+ { value: "powerline", label: "Powerline" },
339
+ { value: "powerline-thin", label: "Thin chevron" },
340
+ { value: "slash", label: "Slash" },
341
+ { value: "pipe", label: "Pipe" },
342
+ { value: "block", label: "Block" },
343
+ { value: "none", label: "None" },
344
+ { value: "ascii", label: "ASCII" },
345
+ ],
346
+ currentValue,
347
+ done,
348
+ () => done(),
349
+ ),
350
+ },
351
+ ];
352
+
353
+ for (const id of PUBLIC_STATUS_SEGMENTS) {
354
+ items.push({
355
+ id: `segment.${id}`,
356
+ label: `Segment: ${statusSegmentLabel(id)}`,
357
+ description: "Cycle placement: hidden → left → right. Use move actions below to reorder visible segments.",
358
+ currentValue: segmentPlacement(id, this.#draft.leftSegments, this.#draft.rightSegments),
359
+ values: ["hidden", "left", "right"],
360
+ });
361
+ if (segmentPlacement(id, this.#draft.leftSegments, this.#draft.rightSegments) !== "hidden") {
362
+ items.push(
363
+ {
364
+ id: `moveup.${id}`,
365
+ label: `Move left: ${statusSegmentLabel(id)}`,
366
+ currentValue: "←",
367
+ values: ["←"],
368
+ },
369
+ {
370
+ id: `movedown.${id}`,
371
+ label: `Move right: ${statusSegmentLabel(id)}`,
372
+ currentValue: "→",
373
+ values: ["→"],
374
+ },
375
+ );
376
+ }
377
+ if (id === "usage") {
378
+ items.push({
379
+ id: "option.usage.mode",
380
+ label: "Usage: mode",
381
+ currentValue: this.#draft.segmentOptions.usage?.mode ?? "used",
382
+ submenu: (currentValue, done) =>
383
+ new SelectSubmenu(
384
+ "Usage mode",
385
+ "Show used quota or remaining quota in the usage segment.",
386
+ USAGE_MODE_OPTIONS,
387
+ currentValue,
388
+ done,
389
+ () => done(),
390
+ ),
391
+ });
392
+ }
393
+ }
394
+
395
+ items.push(
396
+ {
397
+ id: "option.model.showThinkingLevel",
398
+ label: "Model: show thinking level",
399
+ currentValue: String(this.#draft.segmentOptions.model?.showThinkingLevel !== false),
400
+ values: BOOL_VALUES,
401
+ },
402
+ {
403
+ id: "option.path.abbreviate",
404
+ label: "Path: abbreviate",
405
+ currentValue: String(this.#draft.segmentOptions.path?.abbreviate !== false),
406
+ values: BOOL_VALUES,
407
+ },
408
+ {
409
+ id: "option.path.maxLength",
410
+ label: "Path: max length",
411
+ currentValue: String(this.#draft.segmentOptions.path?.maxLength ?? 32),
412
+ submenu: (currentValue, done) =>
413
+ new SelectSubmenu(
414
+ "Path max length",
415
+ "Maximum displayed path length.",
416
+ PATH_LENGTH_OPTIONS,
417
+ currentValue,
418
+ done,
419
+ () => done(),
420
+ ),
421
+ },
422
+ {
423
+ id: "option.path.stripWorkPrefix",
424
+ label: "Path: strip work prefix",
425
+ currentValue: String(this.#draft.segmentOptions.path?.stripWorkPrefix === true),
426
+ values: BOOL_VALUES,
427
+ },
428
+ {
429
+ id: "option.git.showBranch",
430
+ label: "Git: show branch",
431
+ currentValue: String(this.#draft.segmentOptions.git?.showBranch !== false),
432
+ values: BOOL_VALUES,
433
+ },
434
+ {
435
+ id: "option.git.showStaged",
436
+ label: "Git: show staged",
437
+ currentValue: String(this.#draft.segmentOptions.git?.showStaged !== false),
438
+ values: BOOL_VALUES,
439
+ },
440
+ {
441
+ id: "option.git.showUnstaged",
442
+ label: "Git: show unstaged",
443
+ currentValue: String(this.#draft.segmentOptions.git?.showUnstaged !== false),
444
+ values: BOOL_VALUES,
445
+ },
446
+ {
447
+ id: "option.git.showUntracked",
448
+ label: "Git: show untracked",
449
+ currentValue: String(this.#draft.segmentOptions.git?.showUntracked !== false),
450
+ values: BOOL_VALUES,
451
+ },
452
+ {
453
+ id: "option.time.format",
454
+ label: "Time: format",
455
+ currentValue: this.#draft.segmentOptions.time?.format ?? "24h",
456
+ submenu: (currentValue, done) =>
457
+ new SelectSubmenu(
458
+ "Time format",
459
+ "Clock format for the time segment.",
460
+ TIME_FORMAT_OPTIONS,
461
+ currentValue,
462
+ done,
463
+ () => done(),
464
+ ),
465
+ },
466
+ {
467
+ id: "option.time.showSeconds",
468
+ label: "Time: show seconds",
469
+ currentValue: String(this.#draft.segmentOptions.time?.showSeconds === true),
470
+ values: BOOL_VALUES,
471
+ },
472
+ );
473
+
474
+ return items;
475
+ }
476
+
477
+ #handleChange(id: string, value: string): void {
478
+ if (id === "action.save") {
479
+ this.#save();
480
+ return;
481
+ }
482
+ if (id === "action.cancel") {
483
+ this.#cancel();
484
+ return;
485
+ }
486
+ if (id === "separator") {
487
+ this.#draft.separator = value as StatusLineSeparatorStyle;
488
+ } else if (id.startsWith("segment.")) {
489
+ this.#setSegmentPlacement(
490
+ id.slice("segment.".length) as StatusLineSegmentId,
491
+ value as "hidden" | "left" | "right",
492
+ );
493
+ } else if (id.startsWith("moveup.")) {
494
+ this.#moveSegment(id.slice("moveup.".length) as StatusLineSegmentId, -1);
495
+ } else if (id.startsWith("movedown.")) {
496
+ this.#moveSegment(id.slice("movedown.".length) as StatusLineSegmentId, 1);
497
+ } else if (id.startsWith("option.")) {
498
+ this.#setOption(id.slice("option.".length), value);
499
+ }
500
+ this.#preview();
501
+ this.#refresh();
502
+ }
503
+
504
+ #setSegmentPlacement(id: StatusLineSegmentId, placement: "hidden" | "left" | "right"): void {
505
+ this.#draft.leftSegments = this.#draft.leftSegments.filter(segment => segment !== id);
506
+ this.#draft.rightSegments = this.#draft.rightSegments.filter(segment => segment !== id);
507
+ if (placement === "left") this.#draft.leftSegments.push(id);
508
+ if (placement === "right") this.#draft.rightSegments.push(id);
509
+ }
510
+
511
+ #moveSegment(id: StatusLineSegmentId, delta: -1 | 1): void {
512
+ const group = this.#draft.leftSegments.includes(id) ? this.#draft.leftSegments : this.#draft.rightSegments;
513
+ const index = group.indexOf(id);
514
+ if (index < 0) return;
515
+ const nextIndex = Math.max(0, Math.min(group.length - 1, index + delta));
516
+ if (nextIndex === index) return;
517
+ const [segment] = group.splice(index, 1);
518
+ if (segment) group.splice(nextIndex, 0, segment);
519
+ }
520
+
521
+ #setOption(path: string, value: string): void {
522
+ const bool = value === "true";
523
+ switch (path) {
524
+ case "model.showThinkingLevel":
525
+ this.#draft.segmentOptions.model = { ...(this.#draft.segmentOptions.model ?? {}), showThinkingLevel: bool };
526
+ break;
527
+ case "path.abbreviate":
528
+ this.#draft.segmentOptions.path = { ...(this.#draft.segmentOptions.path ?? {}), abbreviate: bool };
529
+ break;
530
+ case "path.maxLength":
531
+ this.#draft.segmentOptions.path = { ...(this.#draft.segmentOptions.path ?? {}), maxLength: Number(value) };
532
+ break;
533
+ case "path.stripWorkPrefix":
534
+ this.#draft.segmentOptions.path = { ...(this.#draft.segmentOptions.path ?? {}), stripWorkPrefix: bool };
535
+ break;
536
+ case "git.showBranch":
537
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showBranch: bool };
538
+ break;
539
+ case "git.showStaged":
540
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showStaged: bool };
541
+ break;
542
+ case "git.showUnstaged":
543
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showUnstaged: bool };
544
+ break;
545
+ case "git.showUntracked":
546
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showUntracked: bool };
547
+ break;
548
+ case "time.format":
549
+ this.#draft.segmentOptions.time = {
550
+ ...(this.#draft.segmentOptions.time ?? {}),
551
+ format: value as "12h" | "24h",
552
+ };
553
+ break;
554
+ case "time.showSeconds":
555
+ this.#draft.segmentOptions.time = { ...(this.#draft.segmentOptions.time ?? {}), showSeconds: bool };
556
+ break;
557
+ case "usage.mode":
558
+ this.#draft.segmentOptions.usage = {
559
+ ...(this.#draft.segmentOptions.usage ?? {}),
560
+ mode: value === "remaining" ? "remaining" : "used",
561
+ };
562
+ break;
563
+ }
564
+ }
565
+
566
+ #preview(): void {
567
+ this.callbacks.onStatusLinePreview?.({
568
+ preset: "custom",
569
+ leftSegments: [...this.#draft.leftSegments],
570
+ rightSegments: [...this.#draft.rightSegments],
571
+ separator: this.#draft.separator,
572
+ segmentOptions: cloneSegmentOptions(this.#draft.segmentOptions),
573
+ previewHighlightSegment: this.#previewHighlightSegment,
574
+ });
575
+ }
576
+
577
+ #restorePreview(): void {
578
+ this.callbacks.onStatusLinePreview?.({
579
+ preset: settings.get("statusLine.preset"),
580
+ leftSegments: settings.get("statusLine.leftSegments"),
581
+ rightSegments: settings.get("statusLine.rightSegments"),
582
+ separator: settings.get("statusLine.separator"),
583
+ segmentOptions: cloneSegmentOptions(settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions),
584
+ sessionAccent: settings.get("statusLine.sessionAccent"),
585
+ previewHighlightSegment: undefined,
586
+ });
587
+ }
588
+
589
+ #save(): void {
590
+ settings.set("statusLine.preset", "custom");
591
+ settings.set("statusLine.leftSegments", [...this.#draft.leftSegments]);
592
+ settings.set("statusLine.rightSegments", [...this.#draft.rightSegments]);
593
+ settings.set("statusLine.separator", this.#draft.separator);
594
+ settings.set(
595
+ "statusLine.segmentOptions",
596
+ cloneSegmentOptions(this.#draft.segmentOptions) as Record<string, unknown>,
597
+ );
598
+ this.callbacks.onChange("statusLine.preset", "custom");
599
+ this.callbacks.onChange("statusLine.leftSegments", [...this.#draft.leftSegments]);
600
+ this.callbacks.onChange("statusLine.rightSegments", [...this.#draft.rightSegments]);
601
+ this.callbacks.onChange("statusLine.separator", this.#draft.separator);
602
+ this.callbacks.onChange("statusLine.segmentOptions", cloneSegmentOptions(this.#draft.segmentOptions));
603
+ this.#previewHighlightSegment = undefined;
604
+ this.#preview();
605
+ this.done("saved");
606
+ }
607
+
608
+ #cancel(): void {
609
+ this.#restorePreview();
610
+ this.done();
611
+ }
612
+
613
+ handleInput(data: string): void {
614
+ this.#list.handleInput(data);
615
+ }
616
+ }
164
617
 
165
618
  function getSettingsTabs(): Tab[] {
166
619
  return [
@@ -194,6 +647,8 @@ export interface StatusLinePreviewSettings {
194
647
  leftSegments?: StatusLineSegmentId[];
195
648
  rightSegments?: StatusLineSegmentId[];
196
649
  separator?: StatusLineSeparatorStyle;
650
+ segmentOptions?: StatusLineSegmentOptions;
651
+ previewHighlightSegment?: StatusLineSegmentId;
197
652
  sessionAccent?: boolean;
198
653
  }
199
654
 
@@ -207,7 +662,7 @@ export interface SettingsCallbacks {
207
662
  /** Called for status line preview while configuring */
208
663
  onStatusLinePreview?: (settings: StatusLinePreviewSettings) => void;
209
664
  /** Get current rendered status line for inline preview */
210
- getStatusLinePreview?: () => string;
665
+ getStatusLinePreview?: (width?: number) => string;
211
666
  /** Called when plugins change */
212
667
  onPluginsChanged?: () => void;
213
668
  /** Called when settings panel is closed */
@@ -372,7 +827,9 @@ export class SettingsSelectorComponent extends Container {
372
827
  } else if (def.path === "theme.dark" || def.path === "theme.light") {
373
828
  options = this.context.availableThemes.map(t => ({ value: t, label: t }));
374
829
  }
375
-
830
+ if (def.path === "statusLine.preset") {
831
+ options = options.filter(option => option.value !== "custom");
832
+ }
376
833
  // Preview handlers
377
834
  let onPreview: ((value: string) => void | Promise<void>) | undefined;
378
835
  let onPreviewCancel: (() => void) | undefined;
@@ -395,17 +852,31 @@ export class SettingsSelectorComponent extends Container {
395
852
  leftSegments: presetDef.leftSegments,
396
853
  rightSegments: presetDef.rightSegments,
397
854
  separator: presetDef.separator,
855
+ previewHighlightSegment: undefined,
398
856
  });
399
857
  this.#updateStatusPreview();
400
858
  };
401
859
  onPreviewCancel = () => {
402
860
  const currentPreset = settings.get("statusLine.preset");
403
861
  const presetDef = getPreset(currentPreset);
862
+ const savedCustomSettings =
863
+ currentPreset === "custom"
864
+ ? {
865
+ leftSegments: settings.get("statusLine.leftSegments"),
866
+ rightSegments: settings.get("statusLine.rightSegments"),
867
+ separator: settings.get("statusLine.separator"),
868
+ segmentOptions: cloneSegmentOptions(
869
+ settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions,
870
+ ),
871
+ }
872
+ : {};
404
873
  this.callbacks.onStatusLinePreview?.({
405
874
  preset: currentPreset,
406
875
  leftSegments: presetDef.leftSegments,
407
876
  rightSegments: presetDef.rightSegments,
408
877
  separator: presetDef.separator,
878
+ ...savedCustomSettings,
879
+ previewHighlightSegment: undefined,
409
880
  });
410
881
  this.#updateStatusPreview();
411
882
  };
@@ -416,7 +887,7 @@ export class SettingsSelectorComponent extends Container {
416
887
  };
417
888
  onPreviewCancel = () => {
418
889
  const separator = settings.get("statusLine.separator");
419
- this.callbacks.onStatusLinePreview?.({ separator });
890
+ this.callbacks.onStatusLinePreview?.({ separator, previewHighlightSegment: undefined });
420
891
  this.#updateStatusPreview();
421
892
  };
422
893
  }
@@ -509,7 +980,7 @@ export class SettingsSelectorComponent extends Container {
509
980
  }
510
981
 
511
982
  this.#currentList = new SettingsList(
512
- this.#buildItemsForDefs(defs),
983
+ this.#buildItemsForTab(defs, tabId),
513
984
  10,
514
985
  getSettingsListTheme(),
515
986
  (id, newValue) => {
@@ -553,10 +1024,38 @@ export class SettingsSelectorComponent extends Container {
553
1024
  return items;
554
1025
  }
555
1026
 
1027
+ #buildItemsForTab(defs: SettingDef[], tabId: SettingTab): SettingItem[] {
1028
+ const items = this.#buildItemsForDefs(defs);
1029
+ if (tabId === "appearance") {
1030
+ const customEditorCallbacks: SettingsCallbacks = {
1031
+ ...this.callbacks,
1032
+ onStatusLinePreview: previewSettings => {
1033
+ this.callbacks.onStatusLinePreview?.(previewSettings);
1034
+ this.#updateStatusPreview();
1035
+ },
1036
+ };
1037
+ const customEditorItem: SettingItem = {
1038
+ id: STATUS_LINE_CUSTOM_EDITOR_ID,
1039
+ label: "Status Line Custom Editor",
1040
+ description:
1041
+ "Edit custom status line segments, placement, separator, and typed segment options with live previews.",
1042
+ currentValue: "open",
1043
+ submenu: (_currentValue, done) => new StatusLineCustomEditor(customEditorCallbacks, done),
1044
+ };
1045
+ const presetIndex = items.findIndex(item => item.id === "statusLine.preset");
1046
+ if (presetIndex >= 0) {
1047
+ items.splice(presetIndex + 1, 0, customEditorItem);
1048
+ } else {
1049
+ items.push(customEditorItem);
1050
+ }
1051
+ }
1052
+ return items;
1053
+ }
1054
+
556
1055
  /** Re-evaluate condition gates against the current settings and refresh the active list. */
557
1056
  #refreshCurrentTabItems(defs: SettingDef[]): void {
558
1057
  if (this.#currentTabId === "plugins" || !this.#currentList) return;
559
- this.#currentList.setItems(this.#buildItemsForDefs(defs));
1058
+ this.#currentList.setItems(this.#buildItemsForTab(defs, this.#currentTabId));
560
1059
  }
561
1060
 
562
1061
  /**
@@ -578,7 +1077,9 @@ export class SettingsSelectorComponent extends Container {
578
1077
  leftSegments: settings.get("statusLine.leftSegments"),
579
1078
  rightSegments: settings.get("statusLine.rightSegments"),
580
1079
  separator: settings.get("statusLine.separator"),
1080
+ segmentOptions: cloneSegmentOptions(settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions),
581
1081
  sessionAccent: settings.get("statusLine.sessionAccent"),
1082
+ previewHighlightSegment: undefined,
582
1083
  };
583
1084
  this.callbacks.onStatusLinePreview?.(statusLineSettings);
584
1085
  this.#updateStatusPreview();
@@ -480,7 +480,12 @@ const sessionNameSegment: StatusLineSegment = {
480
480
  },
481
481
  };
482
482
 
483
- function pickUsageColor(percent: number): "muted" | "warning" | "error" {
483
+ function pickUsageColor(percent: number, mode: "used" | "remaining"): "muted" | "warning" | "error" {
484
+ if (mode === "remaining") {
485
+ if (percent <= 20) return "error";
486
+ if (percent <= 50) return "warning";
487
+ return "muted";
488
+ }
484
489
  if (percent >= 80) return "error";
485
490
  if (percent >= 50) return "warning";
486
491
  return "muted";
@@ -505,28 +510,20 @@ const usageSegment: StatusLineSegment = {
505
510
  id: "usage",
506
511
  render(ctx) {
507
512
  const u = ctx.usage;
508
- if (!u || (!u.fiveHour && !u.sevenDay)) {
513
+ if (!u || u.windows.length === 0) {
509
514
  return { content: "", visible: false };
510
515
  }
511
- const parts: string[] = [];
512
- if (u.fiveHour) {
513
- const pct = u.fiveHour.percent;
514
- const pctText = theme.fg(pickUsageColor(pct), `${Math.round(pct)}%`);
515
- const reset =
516
- u.fiveHour.resetMinutes !== undefined
517
- ? theme.fg("muted", ` (${formatUsageReset(u.fiveHour.resetMinutes, "m")})`)
518
- : "";
519
- parts.push(`5h ${pctText}${reset}`);
520
- }
521
- if (u.sevenDay) {
522
- const pct = u.sevenDay.percent;
523
- const pctText = theme.fg(pickUsageColor(pct), `${Math.round(pct)}%`);
516
+ const mode = ctx.options.usage?.mode === "remaining" ? "remaining" : "used";
517
+ const parts = u.windows.map(window => {
518
+ const displayPercent =
519
+ mode === "remaining" ? Math.max(0, Math.min(100, 100 - window.percent)) : window.percent;
520
+ const pctText = theme.fg(pickUsageColor(displayPercent, mode), `${Math.round(displayPercent)}%`);
524
521
  const reset =
525
- u.sevenDay.resetHours !== undefined
526
- ? theme.fg("muted", ` (${formatUsageReset(u.sevenDay.resetHours, "h")})`)
522
+ window.resetValue !== undefined && window.resetUnit !== undefined
523
+ ? theme.fg("muted", ` (${formatUsageReset(window.resetValue, window.resetUnit)})`)
527
524
  : "";
528
- parts.push(`7d ${pctText}${reset}`);
529
- }
525
+ return `${window.label} ${pctText}${reset}`;
526
+ });
530
527
  const content = withIcon(theme.icon.time, parts.join(theme.sep.dot));
531
528
  return { content, visible: true };
532
529
  },
@@ -51,8 +51,12 @@ export interface SegmentContext {
51
51
  pr: { number: number; url: string } | null;
52
52
  };
53
53
  usage: {
54
- fiveHour?: { percent: number; resetMinutes?: number };
55
- sevenDay?: { percent: number; resetHours?: number };
54
+ windows: Array<{
55
+ label: string;
56
+ percent: number;
57
+ resetValue?: number;
58
+ resetUnit?: "m" | "h";
59
+ }>;
56
60
  } | null;
57
61
  }
58
62