@gajae-code/coding-agent 0.7.7 → 0.7.8

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 (63) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +8 -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/model-registry.d.ts +5 -0
  8. package/dist/types/config/models-config-schema.d.ts +5 -0
  9. package/dist/types/config/settings-schema.d.ts +45 -0
  10. package/dist/types/config/settings.d.ts +7 -0
  11. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  12. package/dist/types/gjc-runtime/launch-tmux.d.ts +2 -2
  13. package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
  14. package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
  15. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  16. package/dist/types/modes/components/model-selector.d.ts +2 -0
  17. package/dist/types/modes/components/settings-selector.d.ts +4 -1
  18. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  19. package/dist/types/modes/components/status-line.d.ts +4 -3
  20. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  21. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  22. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  23. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  24. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  25. package/dist/types/notifications/telegram-daemon-cli.d.ts +13 -2
  26. package/package.json +7 -7
  27. package/src/cli/local-provider-smoke.ts +632 -0
  28. package/src/cli/notify-cli.ts +4 -2
  29. package/src/cli/update-cli.ts +155 -13
  30. package/src/cli.ts +18 -0
  31. package/src/commands/launch.ts +46 -1
  32. package/src/commands/local-provider.ts +62 -0
  33. package/src/commands/notify.ts +1 -1
  34. package/src/config/model-registry.ts +55 -1
  35. package/src/config/models-config-schema.ts +9 -0
  36. package/src/config/settings-schema.ts +47 -0
  37. package/src/config/settings.ts +11 -0
  38. package/src/coordinator-mcp/server.ts +276 -34
  39. package/src/gjc-runtime/launch-tmux.ts +34 -8
  40. package/src/gjc-runtime/launch-worktree.ts +16 -2
  41. package/src/gjc-runtime/team-runtime.ts +21 -6
  42. package/src/gjc-runtime/tmux-common.ts +16 -0
  43. package/src/gjc-runtime/tmux-sessions.ts +13 -9
  44. package/src/internal-urls/docs-index.generated.ts +5 -4
  45. package/src/modes/components/agent-dashboard.ts +2 -1
  46. package/src/modes/components/custom-model-preset-wizard.ts +30 -189
  47. package/src/modes/components/model-selector.ts +182 -5
  48. package/src/modes/components/session-observer-overlay.ts +3 -3
  49. package/src/modes/components/settings-selector.ts +491 -5
  50. package/src/modes/components/status-line/segments.ts +7 -18
  51. package/src/modes/components/status-line/types.ts +6 -2
  52. package/src/modes/components/status-line.ts +69 -35
  53. package/src/modes/controllers/event-controller.ts +98 -4
  54. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  55. package/src/modes/controllers/input-controller.ts +15 -0
  56. package/src/modes/controllers/selector-controller.ts +12 -4
  57. package/src/modes/prompt-action-autocomplete.ts +8 -0
  58. package/src/modes/tmux-scroll.ts +92 -0
  59. package/src/modes/utils/terminal-bell.ts +48 -0
  60. package/src/notifications/telegram-daemon-cli.ts +112 -9
  61. package/src/notifications/telegram-daemon.ts +4 -0
  62. package/src/session/agent-session.ts +74 -5
  63. package/src/slash-commands/builtin-registry.ts +106 -13
@@ -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,449 @@ 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
+
183
+ function cloneSegmentOptions(options: StatusLineSegmentOptions | undefined): StatusLineSegmentOptions {
184
+ return mergeSegmentOptions(undefined, options);
185
+ }
186
+
187
+ function mergeSegmentOptions(
188
+ base: StatusLineSegmentOptions | undefined,
189
+ overrides: StatusLineSegmentOptions | undefined,
190
+ ): StatusLineSegmentOptions {
191
+ return {
192
+ model: base?.model || overrides?.model ? { ...(base?.model ?? {}), ...(overrides?.model ?? {}) } : undefined,
193
+ path: base?.path || overrides?.path ? { ...(base?.path ?? {}), ...(overrides?.path ?? {}) } : undefined,
194
+ git: base?.git || overrides?.git ? { ...(base?.git ?? {}), ...(overrides?.git ?? {}) } : undefined,
195
+ time: base?.time || overrides?.time ? { ...(base?.time ?? {}), ...(overrides?.time ?? {}) } : undefined,
196
+ };
197
+ }
198
+
199
+ function effectiveSegmentOptions(
200
+ preset: StatusLinePreset,
201
+ options: StatusLineSegmentOptions | undefined,
202
+ ): StatusLineSegmentOptions {
203
+ return mergeSegmentOptions(getPreset(preset).segmentOptions, options);
204
+ }
205
+
206
+ function effectiveCustomSegments(
207
+ preset: StatusLinePreset,
208
+ leftSegments: StatusLineSegmentId[],
209
+ rightSegments: StatusLineSegmentId[],
210
+ ): { leftSegments: StatusLineSegmentId[]; rightSegments: StatusLineSegmentId[] } {
211
+ if (preset === "custom") {
212
+ return { leftSegments: [...leftSegments], rightSegments: [...rightSegments] };
213
+ }
214
+ const presetDef = getPreset(preset);
215
+ return {
216
+ leftSegments: [...presetDef.leftSegments],
217
+ rightSegments: [...presetDef.rightSegments],
218
+ };
219
+ }
220
+
221
+ function statusSegmentLabel(id: StatusLineSegmentId): string {
222
+ return id.replace(/_/g, " ");
223
+ }
224
+
225
+ function segmentPlacement(
226
+ id: StatusLineSegmentId,
227
+ leftSegments: StatusLineSegmentId[],
228
+ rightSegments: StatusLineSegmentId[],
229
+ ): "hidden" | "left" | "right" {
230
+ if (leftSegments.includes(id)) return "left";
231
+ if (rightSegments.includes(id)) return "right";
232
+ return "hidden";
233
+ }
234
+
235
+ class StatusLineCustomEditor extends Container {
236
+ #list!: SettingsList;
237
+ #draft: StatusLineDraft;
238
+ #currentWidthPreviewText!: Text;
239
+ #narrowWidthPreviewText!: Text;
240
+ #previewHighlightSegment: StatusLineSegmentId | undefined;
241
+
242
+ constructor(
243
+ private readonly callbacks: SettingsCallbacks,
244
+ private readonly done: (value?: string) => void,
245
+ ) {
246
+ super();
247
+ const preset = settings.get("statusLine.preset");
248
+ const seeded = effectiveCustomSegments(
249
+ preset,
250
+ settings.get("statusLine.leftSegments"),
251
+ settings.get("statusLine.rightSegments"),
252
+ );
253
+ this.#draft = {
254
+ preset: "custom",
255
+ leftSegments: seeded.leftSegments,
256
+ rightSegments: seeded.rightSegments,
257
+ separator: settings.get("statusLine.separator"),
258
+ segmentOptions: effectiveSegmentOptions(
259
+ preset,
260
+ settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions,
261
+ ),
262
+ };
263
+ this.#preview();
264
+ this.#build();
265
+ }
266
+
267
+ #build(): void {
268
+ this.clear();
269
+ this.addChild(new Text(theme.bold(theme.fg("accent", "Status Line Custom Editor")), 0, 0));
270
+ this.addChild(new Spacer(1));
271
+ this.addChild(new Text(theme.fg("muted", "Current width preview:"), 0, 0));
272
+ this.#currentWidthPreviewText = new Text(this.#statusLinePreview(), 0, 0);
273
+ this.addChild(this.#currentWidthPreviewText);
274
+ this.addChild(new Text(theme.fg("muted", "Narrow width preview:"), 0, 0));
275
+ this.#narrowWidthPreviewText = new Text(this.#statusLinePreview(40), 0, 0);
276
+ this.addChild(this.#narrowWidthPreviewText);
277
+ this.addChild(new Spacer(1));
278
+ this.#list = new SettingsList(
279
+ this.#items(),
280
+ 14,
281
+ getSettingsListTheme(),
282
+ (id, value) => this.#handleChange(id, value),
283
+ () => this.#cancel(),
284
+ item => this.#setSelectedItem(item),
285
+ );
286
+ this.addChild(this.#list);
287
+ }
288
+
289
+ #refresh(): void {
290
+ this.#refreshPreview();
291
+ this.#list.setItems(this.#items());
292
+ }
293
+
294
+ #refreshPreview(): void {
295
+ this.#currentWidthPreviewText.setText(this.#statusLinePreview());
296
+ this.#narrowWidthPreviewText.setText(this.#statusLinePreview(40));
297
+ }
298
+
299
+ #statusLinePreview(width?: number): string {
300
+ return this.callbacks.getStatusLinePreview?.(width) ?? theme.fg("dim", "(preview not available)");
301
+ }
302
+ #setSelectedItem(item: SettingItem | undefined): void {
303
+ this.#previewHighlightSegment = this.#highlightSegmentForItem(item);
304
+ this.#preview();
305
+ this.#refreshPreview();
306
+ }
307
+
308
+ #highlightSegmentForItem(item: SettingItem | undefined): StatusLineSegmentId | undefined {
309
+ if (!item) return undefined;
310
+ if (item.id.startsWith("segment.")) {
311
+ return item.id.slice("segment.".length) as StatusLineSegmentId;
312
+ }
313
+ if (item.id.startsWith("moveup.")) {
314
+ return item.id.slice("moveup.".length) as StatusLineSegmentId;
315
+ }
316
+ if (item.id.startsWith("movedown.")) {
317
+ return item.id.slice("movedown.".length) as StatusLineSegmentId;
318
+ }
319
+ if (item.id.startsWith("option.")) {
320
+ const [segment] = item.id.slice("option.".length).split(".");
321
+ return segment as StatusLineSegmentId;
322
+ }
323
+ return undefined;
324
+ }
325
+
326
+ #items(): SettingItem[] {
327
+ const items: SettingItem[] = [
328
+ {
329
+ id: "action.save",
330
+ label: "Save custom status line",
331
+ description: "Persist the previewed custom status line to user config.",
332
+ currentValue: "approve",
333
+ values: ["approve"],
334
+ },
335
+ {
336
+ id: "action.cancel",
337
+ label: "Cancel and restore",
338
+ description: "Close without persisting draft settings and restore the previous preview.",
339
+ currentValue: "restore",
340
+ values: ["restore"],
341
+ },
342
+ {
343
+ id: "separator",
344
+ label: "Separator",
345
+ description: "Separator style between status line segments.",
346
+ currentValue: this.#draft.separator,
347
+ submenu: (currentValue, done) =>
348
+ new SelectSubmenu(
349
+ "Status Line Separator",
350
+ "Style of separators between segments.",
351
+ [
352
+ { value: "powerline", label: "Powerline" },
353
+ { value: "powerline-thin", label: "Thin chevron" },
354
+ { value: "slash", label: "Slash" },
355
+ { value: "pipe", label: "Pipe" },
356
+ { value: "block", label: "Block" },
357
+ { value: "none", label: "None" },
358
+ { value: "ascii", label: "ASCII" },
359
+ ],
360
+ currentValue,
361
+ done,
362
+ () => done(),
363
+ ),
364
+ },
365
+ ];
366
+
367
+ for (const id of PUBLIC_STATUS_SEGMENTS) {
368
+ items.push({
369
+ id: `segment.${id}`,
370
+ label: `Segment: ${statusSegmentLabel(id)}`,
371
+ description: "Cycle placement: hidden → left → right. Use move actions below to reorder visible segments.",
372
+ currentValue: segmentPlacement(id, this.#draft.leftSegments, this.#draft.rightSegments),
373
+ values: ["hidden", "left", "right"],
374
+ });
375
+ if (segmentPlacement(id, this.#draft.leftSegments, this.#draft.rightSegments) !== "hidden") {
376
+ items.push(
377
+ {
378
+ id: `moveup.${id}`,
379
+ label: `Move left: ${statusSegmentLabel(id)}`,
380
+ currentValue: "←",
381
+ values: ["←"],
382
+ },
383
+ {
384
+ id: `movedown.${id}`,
385
+ label: `Move right: ${statusSegmentLabel(id)}`,
386
+ currentValue: "→",
387
+ values: ["→"],
388
+ },
389
+ );
390
+ }
391
+ }
392
+
393
+ items.push(
394
+ {
395
+ id: "option.model.showThinkingLevel",
396
+ label: "Model: show thinking level",
397
+ currentValue: String(this.#draft.segmentOptions.model?.showThinkingLevel !== false),
398
+ values: BOOL_VALUES,
399
+ },
400
+ {
401
+ id: "option.path.abbreviate",
402
+ label: "Path: abbreviate",
403
+ currentValue: String(this.#draft.segmentOptions.path?.abbreviate !== false),
404
+ values: BOOL_VALUES,
405
+ },
406
+ {
407
+ id: "option.path.maxLength",
408
+ label: "Path: max length",
409
+ currentValue: String(this.#draft.segmentOptions.path?.maxLength ?? 32),
410
+ submenu: (currentValue, done) =>
411
+ new SelectSubmenu(
412
+ "Path max length",
413
+ "Maximum displayed path length.",
414
+ PATH_LENGTH_OPTIONS,
415
+ currentValue,
416
+ done,
417
+ () => done(),
418
+ ),
419
+ },
420
+ {
421
+ id: "option.path.stripWorkPrefix",
422
+ label: "Path: strip work prefix",
423
+ currentValue: String(this.#draft.segmentOptions.path?.stripWorkPrefix === true),
424
+ values: BOOL_VALUES,
425
+ },
426
+ {
427
+ id: "option.git.showBranch",
428
+ label: "Git: show branch",
429
+ currentValue: String(this.#draft.segmentOptions.git?.showBranch !== false),
430
+ values: BOOL_VALUES,
431
+ },
432
+ {
433
+ id: "option.git.showStaged",
434
+ label: "Git: show staged",
435
+ currentValue: String(this.#draft.segmentOptions.git?.showStaged !== false),
436
+ values: BOOL_VALUES,
437
+ },
438
+ {
439
+ id: "option.git.showUnstaged",
440
+ label: "Git: show unstaged",
441
+ currentValue: String(this.#draft.segmentOptions.git?.showUnstaged !== false),
442
+ values: BOOL_VALUES,
443
+ },
444
+ {
445
+ id: "option.git.showUntracked",
446
+ label: "Git: show untracked",
447
+ currentValue: String(this.#draft.segmentOptions.git?.showUntracked !== false),
448
+ values: BOOL_VALUES,
449
+ },
450
+ {
451
+ id: "option.time.format",
452
+ label: "Time: format",
453
+ currentValue: this.#draft.segmentOptions.time?.format ?? "24h",
454
+ submenu: (currentValue, done) =>
455
+ new SelectSubmenu(
456
+ "Time format",
457
+ "Clock format for the time segment.",
458
+ TIME_FORMAT_OPTIONS,
459
+ currentValue,
460
+ done,
461
+ () => done(),
462
+ ),
463
+ },
464
+ {
465
+ id: "option.time.showSeconds",
466
+ label: "Time: show seconds",
467
+ currentValue: String(this.#draft.segmentOptions.time?.showSeconds === true),
468
+ values: BOOL_VALUES,
469
+ },
470
+ );
471
+
472
+ return items;
473
+ }
474
+
475
+ #handleChange(id: string, value: string): void {
476
+ if (id === "action.save") {
477
+ this.#save();
478
+ return;
479
+ }
480
+ if (id === "action.cancel") {
481
+ this.#cancel();
482
+ return;
483
+ }
484
+ if (id === "separator") {
485
+ this.#draft.separator = value as StatusLineSeparatorStyle;
486
+ } else if (id.startsWith("segment.")) {
487
+ this.#setSegmentPlacement(
488
+ id.slice("segment.".length) as StatusLineSegmentId,
489
+ value as "hidden" | "left" | "right",
490
+ );
491
+ } else if (id.startsWith("moveup.")) {
492
+ this.#moveSegment(id.slice("moveup.".length) as StatusLineSegmentId, -1);
493
+ } else if (id.startsWith("movedown.")) {
494
+ this.#moveSegment(id.slice("movedown.".length) as StatusLineSegmentId, 1);
495
+ } else if (id.startsWith("option.")) {
496
+ this.#setOption(id.slice("option.".length), value);
497
+ }
498
+ this.#preview();
499
+ this.#refresh();
500
+ }
501
+
502
+ #setSegmentPlacement(id: StatusLineSegmentId, placement: "hidden" | "left" | "right"): void {
503
+ this.#draft.leftSegments = this.#draft.leftSegments.filter(segment => segment !== id);
504
+ this.#draft.rightSegments = this.#draft.rightSegments.filter(segment => segment !== id);
505
+ if (placement === "left") this.#draft.leftSegments.push(id);
506
+ if (placement === "right") this.#draft.rightSegments.push(id);
507
+ }
508
+
509
+ #moveSegment(id: StatusLineSegmentId, delta: -1 | 1): void {
510
+ const group = this.#draft.leftSegments.includes(id) ? this.#draft.leftSegments : this.#draft.rightSegments;
511
+ const index = group.indexOf(id);
512
+ if (index < 0) return;
513
+ const nextIndex = Math.max(0, Math.min(group.length - 1, index + delta));
514
+ if (nextIndex === index) return;
515
+ const [segment] = group.splice(index, 1);
516
+ if (segment) group.splice(nextIndex, 0, segment);
517
+ }
518
+
519
+ #setOption(path: string, value: string): void {
520
+ const bool = value === "true";
521
+ switch (path) {
522
+ case "model.showThinkingLevel":
523
+ this.#draft.segmentOptions.model = { ...(this.#draft.segmentOptions.model ?? {}), showThinkingLevel: bool };
524
+ break;
525
+ case "path.abbreviate":
526
+ this.#draft.segmentOptions.path = { ...(this.#draft.segmentOptions.path ?? {}), abbreviate: bool };
527
+ break;
528
+ case "path.maxLength":
529
+ this.#draft.segmentOptions.path = { ...(this.#draft.segmentOptions.path ?? {}), maxLength: Number(value) };
530
+ break;
531
+ case "path.stripWorkPrefix":
532
+ this.#draft.segmentOptions.path = { ...(this.#draft.segmentOptions.path ?? {}), stripWorkPrefix: bool };
533
+ break;
534
+ case "git.showBranch":
535
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showBranch: bool };
536
+ break;
537
+ case "git.showStaged":
538
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showStaged: bool };
539
+ break;
540
+ case "git.showUnstaged":
541
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showUnstaged: bool };
542
+ break;
543
+ case "git.showUntracked":
544
+ this.#draft.segmentOptions.git = { ...(this.#draft.segmentOptions.git ?? {}), showUntracked: bool };
545
+ break;
546
+ case "time.format":
547
+ this.#draft.segmentOptions.time = {
548
+ ...(this.#draft.segmentOptions.time ?? {}),
549
+ format: value as "12h" | "24h",
550
+ };
551
+ break;
552
+ case "time.showSeconds":
553
+ this.#draft.segmentOptions.time = { ...(this.#draft.segmentOptions.time ?? {}), showSeconds: bool };
554
+ break;
555
+ }
556
+ }
557
+
558
+ #preview(): void {
559
+ this.callbacks.onStatusLinePreview?.({
560
+ preset: "custom",
561
+ leftSegments: [...this.#draft.leftSegments],
562
+ rightSegments: [...this.#draft.rightSegments],
563
+ separator: this.#draft.separator,
564
+ segmentOptions: cloneSegmentOptions(this.#draft.segmentOptions),
565
+ previewHighlightSegment: this.#previewHighlightSegment,
566
+ });
567
+ }
568
+
569
+ #restorePreview(): void {
570
+ this.callbacks.onStatusLinePreview?.({
571
+ preset: settings.get("statusLine.preset"),
572
+ leftSegments: settings.get("statusLine.leftSegments"),
573
+ rightSegments: settings.get("statusLine.rightSegments"),
574
+ separator: settings.get("statusLine.separator"),
575
+ segmentOptions: cloneSegmentOptions(settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions),
576
+ sessionAccent: settings.get("statusLine.sessionAccent"),
577
+ previewHighlightSegment: undefined,
578
+ });
579
+ }
580
+
581
+ #save(): void {
582
+ settings.set("statusLine.preset", "custom");
583
+ settings.set("statusLine.leftSegments", [...this.#draft.leftSegments]);
584
+ settings.set("statusLine.rightSegments", [...this.#draft.rightSegments]);
585
+ settings.set("statusLine.separator", this.#draft.separator);
586
+ settings.set(
587
+ "statusLine.segmentOptions",
588
+ cloneSegmentOptions(this.#draft.segmentOptions) as Record<string, unknown>,
589
+ );
590
+ this.callbacks.onChange("statusLine.preset", "custom");
591
+ this.callbacks.onChange("statusLine.leftSegments", [...this.#draft.leftSegments]);
592
+ this.callbacks.onChange("statusLine.rightSegments", [...this.#draft.rightSegments]);
593
+ this.callbacks.onChange("statusLine.separator", this.#draft.separator);
594
+ this.callbacks.onChange("statusLine.segmentOptions", cloneSegmentOptions(this.#draft.segmentOptions));
595
+ this.#previewHighlightSegment = undefined;
596
+ this.#preview();
597
+ this.done("saved");
598
+ }
599
+
600
+ #cancel(): void {
601
+ this.#restorePreview();
602
+ this.done();
603
+ }
604
+
605
+ handleInput(data: string): void {
606
+ this.#list.handleInput(data);
607
+ }
608
+ }
164
609
 
165
610
  function getSettingsTabs(): Tab[] {
166
611
  return [
@@ -194,6 +639,8 @@ export interface StatusLinePreviewSettings {
194
639
  leftSegments?: StatusLineSegmentId[];
195
640
  rightSegments?: StatusLineSegmentId[];
196
641
  separator?: StatusLineSeparatorStyle;
642
+ segmentOptions?: StatusLineSegmentOptions;
643
+ previewHighlightSegment?: StatusLineSegmentId;
197
644
  sessionAccent?: boolean;
198
645
  }
199
646
 
@@ -207,7 +654,7 @@ export interface SettingsCallbacks {
207
654
  /** Called for status line preview while configuring */
208
655
  onStatusLinePreview?: (settings: StatusLinePreviewSettings) => void;
209
656
  /** Get current rendered status line for inline preview */
210
- getStatusLinePreview?: () => string;
657
+ getStatusLinePreview?: (width?: number) => string;
211
658
  /** Called when plugins change */
212
659
  onPluginsChanged?: () => void;
213
660
  /** Called when settings panel is closed */
@@ -372,7 +819,9 @@ export class SettingsSelectorComponent extends Container {
372
819
  } else if (def.path === "theme.dark" || def.path === "theme.light") {
373
820
  options = this.context.availableThemes.map(t => ({ value: t, label: t }));
374
821
  }
375
-
822
+ if (def.path === "statusLine.preset") {
823
+ options = options.filter(option => option.value !== "custom");
824
+ }
376
825
  // Preview handlers
377
826
  let onPreview: ((value: string) => void | Promise<void>) | undefined;
378
827
  let onPreviewCancel: (() => void) | undefined;
@@ -395,17 +844,31 @@ export class SettingsSelectorComponent extends Container {
395
844
  leftSegments: presetDef.leftSegments,
396
845
  rightSegments: presetDef.rightSegments,
397
846
  separator: presetDef.separator,
847
+ previewHighlightSegment: undefined,
398
848
  });
399
849
  this.#updateStatusPreview();
400
850
  };
401
851
  onPreviewCancel = () => {
402
852
  const currentPreset = settings.get("statusLine.preset");
403
853
  const presetDef = getPreset(currentPreset);
854
+ const savedCustomSettings =
855
+ currentPreset === "custom"
856
+ ? {
857
+ leftSegments: settings.get("statusLine.leftSegments"),
858
+ rightSegments: settings.get("statusLine.rightSegments"),
859
+ separator: settings.get("statusLine.separator"),
860
+ segmentOptions: cloneSegmentOptions(
861
+ settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions,
862
+ ),
863
+ }
864
+ : {};
404
865
  this.callbacks.onStatusLinePreview?.({
405
866
  preset: currentPreset,
406
867
  leftSegments: presetDef.leftSegments,
407
868
  rightSegments: presetDef.rightSegments,
408
869
  separator: presetDef.separator,
870
+ ...savedCustomSettings,
871
+ previewHighlightSegment: undefined,
409
872
  });
410
873
  this.#updateStatusPreview();
411
874
  };
@@ -416,7 +879,7 @@ export class SettingsSelectorComponent extends Container {
416
879
  };
417
880
  onPreviewCancel = () => {
418
881
  const separator = settings.get("statusLine.separator");
419
- this.callbacks.onStatusLinePreview?.({ separator });
882
+ this.callbacks.onStatusLinePreview?.({ separator, previewHighlightSegment: undefined });
420
883
  this.#updateStatusPreview();
421
884
  };
422
885
  }
@@ -509,7 +972,7 @@ export class SettingsSelectorComponent extends Container {
509
972
  }
510
973
 
511
974
  this.#currentList = new SettingsList(
512
- this.#buildItemsForDefs(defs),
975
+ this.#buildItemsForTab(defs, tabId),
513
976
  10,
514
977
  getSettingsListTheme(),
515
978
  (id, newValue) => {
@@ -553,10 +1016,31 @@ export class SettingsSelectorComponent extends Container {
553
1016
  return items;
554
1017
  }
555
1018
 
1019
+ #buildItemsForTab(defs: SettingDef[], tabId: SettingTab): SettingItem[] {
1020
+ const items = this.#buildItemsForDefs(defs);
1021
+ if (tabId === "appearance") {
1022
+ const customEditorItem: SettingItem = {
1023
+ id: STATUS_LINE_CUSTOM_EDITOR_ID,
1024
+ label: "Status Line Custom Editor",
1025
+ description:
1026
+ "Edit custom status line segments, placement, separator, and typed segment options with live previews.",
1027
+ currentValue: "open",
1028
+ submenu: (_currentValue, done) => new StatusLineCustomEditor(this.callbacks, done),
1029
+ };
1030
+ const presetIndex = items.findIndex(item => item.id === "statusLine.preset");
1031
+ if (presetIndex >= 0) {
1032
+ items.splice(presetIndex + 1, 0, customEditorItem);
1033
+ } else {
1034
+ items.push(customEditorItem);
1035
+ }
1036
+ }
1037
+ return items;
1038
+ }
1039
+
556
1040
  /** Re-evaluate condition gates against the current settings and refresh the active list. */
557
1041
  #refreshCurrentTabItems(defs: SettingDef[]): void {
558
1042
  if (this.#currentTabId === "plugins" || !this.#currentList) return;
559
- this.#currentList.setItems(this.#buildItemsForDefs(defs));
1043
+ this.#currentList.setItems(this.#buildItemsForTab(defs, this.#currentTabId));
560
1044
  }
561
1045
 
562
1046
  /**
@@ -578,7 +1062,9 @@ export class SettingsSelectorComponent extends Container {
578
1062
  leftSegments: settings.get("statusLine.leftSegments"),
579
1063
  rightSegments: settings.get("statusLine.rightSegments"),
580
1064
  separator: settings.get("statusLine.separator"),
1065
+ segmentOptions: cloneSegmentOptions(settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions),
581
1066
  sessionAccent: settings.get("statusLine.sessionAccent"),
1067
+ previewHighlightSegment: undefined,
582
1068
  };
583
1069
  this.callbacks.onStatusLinePreview?.(statusLineSettings);
584
1070
  this.#updateStatusPreview();
@@ -505,28 +505,17 @@ const usageSegment: StatusLineSegment = {
505
505
  id: "usage",
506
506
  render(ctx) {
507
507
  const u = ctx.usage;
508
- if (!u || (!u.fiveHour && !u.sevenDay)) {
508
+ if (!u || u.windows.length === 0) {
509
509
  return { content: "", visible: false };
510
510
  }
511
- const parts: string[] = [];
512
- if (u.fiveHour) {
513
- const pct = u.fiveHour.percent;
514
- const pctText = theme.fg(pickUsageColor(pct), `${Math.round(pct)}%`);
511
+ const parts = u.windows.map(window => {
512
+ const pctText = theme.fg(pickUsageColor(window.percent), `${Math.round(window.percent)}%`);
515
513
  const reset =
516
- u.fiveHour.resetMinutes !== undefined
517
- ? theme.fg("muted", ` (${formatUsageReset(u.fiveHour.resetMinutes, "m")})`)
514
+ window.resetValue !== undefined && window.resetUnit !== undefined
515
+ ? theme.fg("muted", ` (${formatUsageReset(window.resetValue, window.resetUnit)})`)
518
516
  : "";
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)}%`);
524
- const reset =
525
- u.sevenDay.resetHours !== undefined
526
- ? theme.fg("muted", ` (${formatUsageReset(u.sevenDay.resetHours, "h")})`)
527
- : "";
528
- parts.push(`7d ${pctText}${reset}`);
529
- }
517
+ return `${window.label} ${pctText}${reset}`;
518
+ });
530
519
  const content = withIcon(theme.icon.time, parts.join(theme.sep.dot));
531
520
  return { content, visible: true };
532
521
  },
@@ -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