@selesai/code 0.7.0 → 0.8.0

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 (72) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -2
  3. package/dist/extensions/agent-browser.test.ts +195 -0
  4. package/dist/extensions/agent-browser.ts +158 -0
  5. package/dist/extensions/inline-skills.test.ts +14 -0
  6. package/dist/extensions/inline-skills.ts +14 -7
  7. package/dist/extensions/package.json +2 -1
  8. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +27 -0
  9. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +153 -0
  10. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +16 -0
  11. package/dist/extensions/pi-powerline-footer/README.md +192 -44
  12. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +19 -4
  13. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +80 -25
  14. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +81 -29
  15. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +1 -1
  16. package/dist/extensions/pi-powerline-footer/cd-command.ts +190 -0
  17. package/dist/extensions/pi-powerline-footer/colors.ts +2 -1
  18. package/dist/extensions/pi-powerline-footer/context-usage.ts +121 -6
  19. package/dist/extensions/pi-powerline-footer/currency-rates.ts +142 -0
  20. package/dist/extensions/pi-powerline-footer/editor-composition.ts +32 -0
  21. package/dist/extensions/pi-powerline-footer/git-status.ts +108 -6
  22. package/dist/extensions/pi-powerline-footer/icons.ts +11 -2
  23. package/dist/extensions/pi-powerline-footer/index.ts +1056 -324
  24. package/dist/extensions/pi-powerline-footer/lifecycle.ts +10 -0
  25. package/dist/extensions/pi-powerline-footer/package.json +10 -6
  26. package/dist/extensions/pi-powerline-footer/paths.ts +22 -0
  27. package/dist/extensions/pi-powerline-footer/powerline-config.ts +297 -15
  28. package/dist/extensions/pi-powerline-footer/presets.ts +6 -25
  29. package/dist/extensions/pi-powerline-footer/queue/store.ts +361 -0
  30. package/dist/extensions/pi-powerline-footer/queue/types.ts +54 -0
  31. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +12 -7
  32. package/dist/extensions/pi-powerline-footer/segments.ts +114 -107
  33. package/dist/extensions/pi-powerline-footer/session-usage.ts +1 -1
  34. package/dist/extensions/pi-powerline-footer/shortcuts.ts +17 -3
  35. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +189 -17
  36. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +144 -0
  37. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +120 -2
  38. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +245 -22
  39. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +48 -0
  40. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +34 -0
  41. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +39 -0
  42. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +92 -0
  43. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +4 -1
  44. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +33 -197
  45. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +79 -0
  46. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +63 -0
  47. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +195 -0
  48. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +14 -0
  49. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +167 -0
  50. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +23 -18
  51. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +16 -1
  52. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +249 -0
  53. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +237 -0
  54. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +199 -0
  55. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +191 -100
  56. package/dist/extensions/pi-powerline-footer/theme.ts +32 -18
  57. package/dist/extensions/pi-powerline-footer/token-stats.ts +304 -0
  58. package/dist/extensions/pi-powerline-footer/tsconfig.json +13 -0
  59. package/dist/extensions/pi-powerline-footer/types.ts +74 -29
  60. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +34 -0
  61. package/dist/extensions/pi-powerline-footer/welcome.ts +65 -10
  62. package/dist/extensions/pi-powerline-footer/working-vibes.ts +44 -11
  63. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  64. package/dist/extensions/pi-subagents/package.json +1 -1
  65. package/dist/extensions/pi-subagents/src/slash/inline-subagents.ts +57 -24
  66. package/dist/extensions/pi-subagents/test/unit/inline-subagents.test.ts +82 -7
  67. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +1 -1
  68. package/dist/extensions/ponytail/index.js +1 -31
  69. package/dist/extensions/ponytail/test/extension.test.js +5 -22
  70. package/dist/extensions/rtk.test.ts +27 -12
  71. package/dist/extensions/rtk.ts +41 -35
  72. package/package.json +1 -1
@@ -0,0 +1,10 @@
1
+ export function shouldShowStartupWelcome(reason: unknown, welcomeEnabled: boolean): boolean {
2
+ return reason === "startup" && welcomeEnabled;
3
+ }
4
+
5
+ export function isStaleExtensionContextError(error: unknown): boolean {
6
+ return error instanceof Error && (
7
+ error.message.includes("This extension instance is stale")
8
+ || error.message.includes("This extension ctx is stale")
9
+ );
10
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-powerline-footer",
3
- "version": "0.5.4",
3
+ "version": "0.12.3",
4
4
  "description": "Powerline-style status bar extension for pi coding agent",
5
5
  "type": "module",
6
6
  "files": [
@@ -9,7 +9,8 @@
9
9
  "*.json",
10
10
  "!progress.md",
11
11
  "bash-mode/**/*.ts",
12
- "fixed-editor/**/*.ts"
12
+ "fixed-editor/**/*.ts",
13
+ "queue/**/*.ts"
13
14
  ],
14
15
  "keywords": [
15
16
  "pi-package",
@@ -26,17 +27,20 @@
26
27
  "url": "git+https://github.com/nicobailon/pi-powerline-footer.git"
27
28
  },
28
29
  "scripts": {
30
+ "typecheck": "tsc --noEmit",
29
31
  "test": "node --experimental-strip-types --test tests/**/*.test.ts"
30
32
  },
31
33
  "peerDependencies": {
32
- "@earendil-works/pi-ai": "^0.75.0",
34
+ "@earendil-works/pi-ai": ">=0.81.0 <0.84.0",
33
35
  "@selesai/code": "*",
34
- "@earendil-works/pi-tui": "^0.75.0"
36
+ "@earendil-works/pi-tui": ">=0.81.0 <0.84.0"
35
37
  },
36
38
  "devDependencies": {
37
- "@earendil-works/pi-ai": "^0.75.0",
39
+ "@earendil-works/pi-ai": "^0.83.0",
38
40
  "@selesai/code": "^0.1.0",
39
- "@earendil-works/pi-tui": "^0.75.0"
41
+ "@earendil-works/pi-tui": "^0.83.0",
42
+ "@types/node": "24.13.3",
43
+ "typescript": "5.9.3"
40
44
  },
41
45
  "pi": {
42
46
  "extensions": [
@@ -0,0 +1,22 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { CONFIG_DIR_NAME, getAgentDir } from "@selesai/code";
5
+
6
+ export function getHomeDir(): string {
7
+ return process.env.HOME || process.env.USERPROFILE || homedir();
8
+ }
9
+
10
+ export function getAgentPath(...segments: string[]): string {
11
+ return join(getAgentDir(), ...segments);
12
+ }
13
+
14
+ export function getLegacyPiPath(...segments: string[]): string {
15
+ return join(getHomeDir(), CONFIG_DIR_NAME, ...segments);
16
+ }
17
+
18
+ export function getAgentSessionDirs(): string[] {
19
+ const primary = getAgentPath("sessions");
20
+ const legacy = getLegacyPiPath("sessions");
21
+ return existsSync(legacy) && legacy !== primary ? [primary, legacy] : [primary];
22
+ }
@@ -1,5 +1,7 @@
1
1
  import { visibleWidth } from "@earendil-works/pi-tui";
2
- import type { ColorValue, CustomItemPosition, CustomStatusItem, PresetDef, StatusLinePreset, StatusLineSegmentId } from "./types.ts";
2
+ import { normalizeCostCurrency } from "./currency-rates.ts";
3
+ import { BUILTIN_STATUS_LINE_SEGMENT_IDS } from "./types.ts";
4
+ import type { ColorValue, CustomItemPosition, CustomStatusItem, PowerlinePlacement, PresetDef, StatusLineLayout, StatusLinePreset, StatusLineSegmentId, StatusLineSegmentOptions, StatusLineSeparatorStyle } from "./types.ts";
3
5
 
4
6
  export interface PowerlineConfig {
5
7
  preset: StatusLinePreset;
@@ -7,6 +9,17 @@ export interface PowerlineConfig {
7
9
  mouseScroll: boolean;
8
10
  fixedEditor: boolean;
9
11
  fixedEditorPromptGlyph: string;
12
+ disabledSegments: StatusLineSegmentId[];
13
+ invalidDisabledSegments: string[];
14
+ layout: StatusLineLayout | null;
15
+ invalidLayoutSegments: string[];
16
+ separator: StatusLineSeparatorStyle | null;
17
+ segmentOptions: StatusLineSegmentOptions;
18
+ placement: PowerlinePlacement;
19
+ invalidPlacement: string | null;
20
+ welcome: boolean;
21
+ stashSharpSShortcut: boolean;
22
+ queue: { captureSigil: string | false };
10
23
  }
11
24
 
12
25
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -19,6 +32,41 @@ function normalizePreset(value: unknown, presets: readonly StatusLinePreset[]):
19
32
  return (presets as readonly string[]).includes(normalized) ? (normalized as StatusLinePreset) : null;
20
33
  }
21
34
 
35
+ function normalizePlacement(value: unknown): { placement: PowerlinePlacement; invalidPlacement: string | null } {
36
+ if (value === undefined) return { placement: "above", invalidPlacement: null };
37
+
38
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
39
+ if (normalized === "above" || normalized === "below") {
40
+ return { placement: normalized, invalidPlacement: null };
41
+ }
42
+
43
+ return {
44
+ placement: "above",
45
+ invalidPlacement: typeof value === "string" ? value.trim() : String(value),
46
+ };
47
+ }
48
+
49
+ const SEPARATOR_STYLES = [
50
+ "powerline",
51
+ "powerline-thin",
52
+ "slash",
53
+ "pipe",
54
+ "block",
55
+ "none",
56
+ "ascii",
57
+ "dot",
58
+ "chevron",
59
+ "star",
60
+ ] as const satisfies readonly StatusLineSeparatorStyle[];
61
+
62
+ function normalizeSeparator(value: unknown): StatusLineSeparatorStyle | null {
63
+ if (typeof value !== "string") return null;
64
+ const normalized = value.trim().toLowerCase();
65
+ return (SEPARATOR_STYLES as readonly string[]).includes(normalized)
66
+ ? (normalized as StatusLineSeparatorStyle)
67
+ : null;
68
+ }
69
+
22
70
  function normalizeCustomItemId(value: unknown): string | null {
23
71
  if (typeof value !== "string") return null;
24
72
  const normalized = value.trim();
@@ -43,6 +91,13 @@ function normalizeCustomPrefix(value: unknown): string | undefined {
43
91
  return normalized ? normalized : undefined;
44
92
  }
45
93
 
94
+ function normalizeCaptureSigil(value: unknown): string | false {
95
+ if (value === false) return false;
96
+ if (typeof value !== "string") return "#";
97
+ const normalized = value.trim();
98
+ return normalized && !/\s/.test(normalized) ? normalized : "#";
99
+ }
100
+
46
101
  function normalizeCustomStatusItem(raw: unknown, idOverride?: string): CustomStatusItem | null {
47
102
  if (!isRecord(raw)) return null;
48
103
  const id = normalizeCustomItemId(idOverride ?? raw.id);
@@ -84,40 +139,267 @@ function normalizeCustomItems(raw: unknown): CustomStatusItem[] {
84
139
  return [...deduped.values()];
85
140
  }
86
141
 
142
+ const BUILTIN_STATUS_LINE_SEGMENT_ID_SET = new Set<string>(BUILTIN_STATUS_LINE_SEGMENT_IDS);
143
+
144
+ function normalizeStatusLineSegmentId(value: unknown, customItemIds: ReadonlySet<string>): StatusLineSegmentId | null {
145
+ if (typeof value !== "string") return null;
146
+
147
+ const normalized = value.trim();
148
+ if (BUILTIN_STATUS_LINE_SEGMENT_ID_SET.has(normalized)) {
149
+ return normalized as StatusLineSegmentId;
150
+ }
151
+
152
+ const customId = normalized.startsWith("custom:")
153
+ ? normalizeCustomItemId(normalized.slice("custom:".length))
154
+ : null;
155
+ return customId && customItemIds.has(customId) ? `custom:${customId}` : null;
156
+ }
157
+
158
+ function normalizeDisabledSegments(
159
+ raw: unknown,
160
+ customItems: readonly CustomStatusItem[],
161
+ ): { disabledSegments: StatusLineSegmentId[]; invalidDisabledSegments: string[] } {
162
+ if (!Array.isArray(raw)) return { disabledSegments: [], invalidDisabledSegments: [] };
163
+
164
+ const disabledSegments: StatusLineSegmentId[] = [];
165
+ const invalidDisabledSegments: string[] = [];
166
+ const customItemIds = new Set(customItems.map((item) => item.id));
167
+ const seen = new Set<StatusLineSegmentId>();
168
+
169
+ for (const entry of raw) {
170
+ const segmentId = normalizeStatusLineSegmentId(entry, customItemIds);
171
+ if (!segmentId) {
172
+ invalidDisabledSegments.push(typeof entry === "string" ? entry.trim() : String(entry));
173
+ } else if (!seen.has(segmentId)) {
174
+ seen.add(segmentId);
175
+ disabledSegments.push(segmentId);
176
+ }
177
+ }
178
+
179
+ return { disabledSegments, invalidDisabledSegments };
180
+ }
181
+
182
+ function normalizeLayout(
183
+ raw: unknown,
184
+ customItems: readonly CustomStatusItem[],
185
+ ): { layout: StatusLineLayout | null; invalidLayoutSegments: string[] } {
186
+ if (!isRecord(raw)) return { layout: null, invalidLayoutSegments: [] };
187
+
188
+ const layout: StatusLineLayout = {};
189
+ const invalidLayoutSegments: string[] = [];
190
+ const customItemIds = new Set(customItems.map((item) => item.id));
191
+ const globallyPlaced = new Set<StatusLineSegmentId>();
192
+
193
+ for (const row of ["left", "right", "secondary"] as const) {
194
+ const entries = raw[row];
195
+ if (!Array.isArray(entries)) continue;
196
+
197
+ const segments: StatusLineSegmentId[] = [];
198
+ const seen = new Set<StatusLineSegmentId>();
199
+ for (const entry of entries) {
200
+ const segmentId = normalizeStatusLineSegmentId(entry, customItemIds);
201
+ if (!segmentId) {
202
+ invalidLayoutSegments.push(`${row}:${typeof entry === "string" ? entry.trim() : String(entry)}`);
203
+ } else if (!seen.has(segmentId)) {
204
+ seen.add(segmentId);
205
+ if (globallyPlaced.has(segmentId)) {
206
+ invalidLayoutSegments.push(`${row}:${segmentId}`);
207
+ } else {
208
+ globallyPlaced.add(segmentId);
209
+ segments.push(segmentId);
210
+ }
211
+ }
212
+ }
213
+ layout[row] = segments;
214
+ }
215
+
216
+ return Object.keys(layout).length > 0
217
+ ? { layout, invalidLayoutSegments }
218
+ : { layout: null, invalidLayoutSegments };
219
+ }
220
+
221
+ function normalizeSegmentOptions(raw: Record<string, unknown>): StatusLineSegmentOptions {
222
+ const options: StatusLineSegmentOptions = {};
223
+
224
+ if (isRecord(raw.model)) {
225
+ options.model = {
226
+ ...(typeof raw.model.showThinkingLevel === "boolean" ? { showThinkingLevel: raw.model.showThinkingLevel } : {}),
227
+ ...(raw.model.display === "name" || raw.model.display === "qualified" ? { display: raw.model.display } : {}),
228
+ };
229
+ }
230
+
231
+ if (isRecord(raw.path)) {
232
+ options.path = {
233
+ ...(raw.path.mode === "basename" || raw.path.mode === "abbreviated" || raw.path.mode === "full" ? { mode: raw.path.mode } : {}),
234
+ ...(typeof raw.path.maxLength === "number" && Number.isFinite(raw.path.maxLength) && raw.path.maxLength > 0
235
+ ? { maxLength: Math.floor(raw.path.maxLength) }
236
+ : {}),
237
+ };
238
+ }
239
+
240
+ if (isRecord(raw.git)) {
241
+ options.git = {
242
+ ...(typeof raw.git.showBranch === "boolean" ? { showBranch: raw.git.showBranch } : {}),
243
+ ...(typeof raw.git.showStaged === "boolean" ? { showStaged: raw.git.showStaged } : {}),
244
+ ...(typeof raw.git.showUnstaged === "boolean" ? { showUnstaged: raw.git.showUnstaged } : {}),
245
+ ...(typeof raw.git.showUntracked === "boolean" ? { showUntracked: raw.git.showUntracked } : {}),
246
+ ...(raw.git.polling === "full" || raw.git.polling === "branch" || raw.git.polling === "off" ? { polling: raw.git.polling } : {}),
247
+ ...(typeof raw.git.hostIcon === "boolean" ? { hostIcon: raw.git.hostIcon } : {}),
248
+ };
249
+ }
250
+
251
+ if (isRecord(raw.time)) {
252
+ options.time = {
253
+ ...(raw.time.format === "12h" || raw.time.format === "24h" ? { format: raw.time.format } : {}),
254
+ ...(typeof raw.time.showSeconds === "boolean" ? { showSeconds: raw.time.showSeconds } : {}),
255
+ };
256
+ }
257
+
258
+ if (isRecord(raw.cost)) {
259
+ const currency = normalizeCostCurrency(raw.cost.currency);
260
+ options.cost = {
261
+ ...(raw.cost.subscriptionDisplay === "subscription"
262
+ || raw.cost.subscriptionDisplay === "reported-cost"
263
+ || raw.cost.subscriptionDisplay === "both"
264
+ ? { subscriptionDisplay: raw.cost.subscriptionDisplay }
265
+ : {}),
266
+ ...(currency ? { currency } : {}),
267
+ };
268
+ }
269
+
270
+ if (isRecord(raw.context)) {
271
+ options.context = {
272
+ ...(raw.context.format === "full" || raw.context.format === "percent" ? { format: raw.context.format } : {}),
273
+ };
274
+ }
275
+
276
+ if (isRecord(raw.cache_read)) {
277
+ options.cache_read = {
278
+ ...(raw.cache_read.format === "tokens" || raw.cache_read.format === "percent" || raw.cache_read.format === "both"
279
+ ? { format: raw.cache_read.format }
280
+ : {}),
281
+ };
282
+ }
283
+
284
+ return options;
285
+ }
286
+
287
+ export function mergeSegmentOptions(
288
+ defaults: StatusLineSegmentOptions = {},
289
+ overrides: StatusLineSegmentOptions = {},
290
+ ): StatusLineSegmentOptions {
291
+ return {
292
+ ...defaults,
293
+ ...overrides,
294
+ model: { ...defaults.model, ...overrides.model },
295
+ path: { ...defaults.path, ...overrides.path },
296
+ git: { ...defaults.git, ...overrides.git },
297
+ time: { ...defaults.time, ...overrides.time },
298
+ cost: { ...defaults.cost, ...overrides.cost },
299
+ context: { ...defaults.context, ...overrides.context },
300
+ cache_read: { ...defaults.cache_read, ...overrides.cache_read },
301
+ };
302
+ }
303
+
87
304
  export function parsePowerlineConfig(value: unknown, presets: readonly StatusLinePreset[]): PowerlineConfig {
88
- const defaultConfig: PowerlineConfig = { preset: "default", customItems: [], mouseScroll: true, fixedEditor: true, fixedEditorPromptGlyph: "" };
305
+ const defaultConfig: PowerlineConfig = {
306
+ preset: "default",
307
+ customItems: [],
308
+ mouseScroll: true,
309
+ fixedEditor: true,
310
+ fixedEditorPromptGlyph: "",
311
+ disabledSegments: [],
312
+ invalidDisabledSegments: [],
313
+ layout: null,
314
+ invalidLayoutSegments: [],
315
+ separator: null,
316
+ segmentOptions: {},
317
+ placement: "above",
318
+ invalidPlacement: null,
319
+ welcome: true,
320
+ stashSharpSShortcut: false,
321
+ queue: { captureSigil: "#" },
322
+ };
89
323
 
90
324
  const directPreset = normalizePreset(value, presets);
91
325
  if (directPreset) return { ...defaultConfig, preset: directPreset };
92
326
 
93
327
  if (!isRecord(value)) return defaultConfig;
94
328
 
329
+ const customItems = normalizeCustomItems(value.customItems);
330
+ const { disabledSegments, invalidDisabledSegments } = normalizeDisabledSegments(value.disabledSegments, customItems);
331
+ const { layout, invalidLayoutSegments } = normalizeLayout(value.layout, customItems);
332
+ const { placement, invalidPlacement } = normalizePlacement(value.placement);
333
+ const queue = isRecord(value.queue)
334
+ ? { captureSigil: normalizeCaptureSigil(value.queue.captureSigil) }
335
+ : defaultConfig.queue;
336
+
95
337
  return {
96
338
  preset: normalizePreset(value.preset, presets) ?? defaultConfig.preset,
97
- customItems: normalizeCustomItems(value.customItems),
339
+ customItems,
98
340
  mouseScroll: value.mouseScroll !== false,
99
341
  fixedEditor: value.fixedEditor !== false,
100
342
  fixedEditorPromptGlyph: typeof value.fixedEditorPromptGlyph === "string" ? value.fixedEditorPromptGlyph : defaultConfig.fixedEditorPromptGlyph,
343
+ disabledSegments,
344
+ invalidDisabledSegments,
345
+ layout,
346
+ invalidLayoutSegments,
347
+ separator: normalizeSeparator(value.separator),
348
+ segmentOptions: normalizeSegmentOptions(value),
349
+ placement,
350
+ invalidPlacement,
351
+ welcome: value.welcome !== false,
352
+ stashSharpSShortcut: value.stashSharpSShortcut === true,
353
+ queue,
101
354
  };
102
355
  }
103
356
 
104
- export function mergeSegmentsWithCustomItems(presetDef: PresetDef, customItems: readonly CustomStatusItem[]): {
357
+ export function mergeSegmentsWithCustomItems(
358
+ presetDef: PresetDef,
359
+ customItems: readonly CustomStatusItem[],
360
+ options: {
361
+ layout?: StatusLineLayout | null;
362
+ disabledSegments?: readonly StatusLineSegmentId[];
363
+ } = {},
364
+ ): {
105
365
  leftSegments: StatusLineSegmentId[];
106
366
  rightSegments: StatusLineSegmentId[];
107
367
  secondarySegments: StatusLineSegmentId[];
108
368
  } {
109
- const left: StatusLineSegmentId[] = [...presetDef.leftSegments];
110
- const right: StatusLineSegmentId[] = [...presetDef.rightSegments];
111
- const secondary: StatusLineSegmentId[] = [...(presetDef.secondarySegments ?? [])];
369
+ const layout = options.layout ?? null;
370
+ const explicitlyPlaced = new Set([
371
+ ...(layout?.left ?? []),
372
+ ...(layout?.right ?? []),
373
+ ...(layout?.secondary ?? []),
374
+ ]);
375
+ const disabled = new Set(options.disabledSegments ?? []);
112
376
 
113
- for (const item of customItems) {
114
- const segmentId: StatusLineSegmentId = `custom:${item.id}`;
115
- if (item.position === "left") left.push(segmentId);
116
- else if (item.position === "secondary") secondary.push(segmentId);
117
- else right.push(segmentId);
118
- }
377
+ const buildRow = (
378
+ position: CustomItemPosition,
379
+ configured: StatusLineSegmentId[] | undefined,
380
+ presetSegments: readonly StatusLineSegmentId[],
381
+ ): StatusLineSegmentId[] => {
382
+ const segments = configured !== undefined
383
+ ? [...configured]
384
+ : presetSegments.filter((id) => !explicitlyPlaced.has(id));
385
+
386
+ if (configured === undefined) {
387
+ for (const item of customItems) {
388
+ const segmentId: StatusLineSegmentId = `custom:${item.id}`;
389
+ if (item.position === position && !explicitlyPlaced.has(segmentId)) {
390
+ segments.push(segmentId);
391
+ }
392
+ }
393
+ }
119
394
 
120
- return { leftSegments: left, rightSegments: right, secondarySegments: secondary };
395
+ return segments.filter((id) => !disabled.has(id));
396
+ };
397
+
398
+ return {
399
+ leftSegments: buildRow("left", layout?.left, presetDef.leftSegments),
400
+ rightSegments: buildRow("right", layout?.right, presetDef.rightSegments),
401
+ secondarySegments: buildRow("secondary", layout?.secondary, presetDef.secondarySegments ?? []),
402
+ };
121
403
  }
122
404
 
123
405
  export function nextPowerlineSettingWithPreset(existingPowerlineSetting: unknown, preset: StatusLinePreset): unknown {
@@ -129,7 +411,7 @@ export function nextPowerlineSettingWithPreset(existingPowerlineSetting: unknown
129
411
 
130
412
  export function nextPowerlineSettingWithOptions(
131
413
  existingPowerlineSetting: unknown,
132
- updates: Partial<Pick<PowerlineConfig, "mouseScroll" | "fixedEditor">>,
414
+ updates: Partial<Pick<PowerlineConfig, "welcome" | "stashSharpSShortcut" | "placement">>,
133
415
  currentPreset: StatusLinePreset,
134
416
  ): unknown {
135
417
  if (!isRecord(existingPowerlineSetting)) {
@@ -23,9 +23,9 @@ const NERD_COLORS: ColorScheme = {
23
23
 
24
24
  export const PRESETS: Record<StatusLinePreset, PresetDef> = {
25
25
  default: {
26
- leftSegments: ["model", "thinking", "shell_mode", "path", "git", "context_pct", "cache_read", "cost"],
26
+ leftSegments: ["model", "thinking", "shell_mode", "path", "git", "queue", "context_pct", "cache_read", "cost"],
27
27
  rightSegments: [],
28
- secondarySegments: ["session_name", "extension_statuses"],
28
+ secondarySegments: ["extension_statuses"],
29
29
  separator: "powerline-thin",
30
30
  colors: DEFAULT_COLORS,
31
31
  segmentOptions: {
@@ -38,7 +38,6 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
38
38
  minimal: {
39
39
  leftSegments: ["shell_mode", "path", "git"],
40
40
  rightSegments: ["context_pct"],
41
- secondarySegments: ["session_name"],
42
41
  separator: "slash",
43
42
  colors: MINIMAL_COLORS,
44
43
  segmentOptions: {
@@ -49,8 +48,7 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
49
48
 
50
49
  compact: {
51
50
  leftSegments: ["model", "shell_mode", "git"],
52
- rightSegments: ["cost", "context_pct"],
53
- secondarySegments: ["session_name"],
51
+ rightSegments: ["queue", "cost", "context_pct"],
54
52
  separator: "powerline-thin",
55
53
  colors: DEFAULT_COLORS,
56
54
  segmentOptions: {
@@ -60,9 +58,8 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
60
58
  },
61
59
 
62
60
  full: {
63
- leftSegments: ["hostname", "model", "thinking", "shell_mode", "path", "git", "subagents"],
61
+ leftSegments: ["hostname", "model", "thinking", "shell_mode", "path", "git", "queue", "subagents"],
64
62
  rightSegments: ["token_in", "token_out", "cache_read", "cost", "context_pct", "time_spent", "time", "extension_statuses"],
65
- secondarySegments: ["session_name"],
66
63
  separator: "powerline",
67
64
  colors: DEFAULT_COLORS,
68
65
  segmentOptions: {
@@ -74,9 +71,8 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
74
71
  },
75
72
 
76
73
  nerd: {
77
- leftSegments: ["hostname", "model", "thinking", "shell_mode", "path", "git", "session", "subagents"],
74
+ leftSegments: ["hostname", "model", "thinking", "shell_mode", "path", "git", "queue", "session", "subagents"],
78
75
  rightSegments: ["token_in", "token_out", "cache_read", "cache_write", "cost", "context_pct", "context_total", "time_spent", "time", "extension_statuses"],
79
- secondarySegments: ["session_name"],
80
76
  separator: "powerline",
81
77
  colors: NERD_COLORS,
82
78
  segmentOptions: {
@@ -89,8 +85,7 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
89
85
 
90
86
  ascii: {
91
87
  leftSegments: ["model", "shell_mode", "path", "git"],
92
- rightSegments: ["token_total", "cost", "context_pct"],
93
- secondarySegments: ["session_name"],
88
+ rightSegments: ["queue", "token_total", "cost", "context_pct"],
94
89
  separator: "ascii",
95
90
  colors: MINIMAL_COLORS,
96
91
  segmentOptions: {
@@ -100,20 +95,6 @@ export const PRESETS: Record<StatusLinePreset, PresetDef> = {
100
95
  },
101
96
  },
102
97
 
103
- custom: {
104
- // {model} {thinking-without-think-prefix} | {path-full-relative-home} | {git} | {subagents}
105
- // | {token_in}({cache_read}) · {token_out} {token_total} · {cost} | {context_pct}
106
- leftSegments: ["identity_group", "path", "git", "subagents"],
107
- rightSegments: ["context_pct"],
108
- secondarySegments: ["session_name", "usage_group"],
109
- separator: "pipe",
110
- colors: DEFAULT_COLORS,
111
- segmentOptions: {
112
- model: { showThinkingLevel: false },
113
- path: { mode: "full" },
114
- git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
115
- },
116
- },
117
98
  };
118
99
 
119
100
  export function getPreset(name: StatusLinePreset): PresetDef {