@selesai/code 0.7.0 → 0.8.1

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 (73) hide show
  1. package/CHANGELOG.md +16 -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 +156 -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/guide.ts +1 -1
  23. package/dist/extensions/pi-powerline-footer/icons.ts +11 -2
  24. package/dist/extensions/pi-powerline-footer/index.ts +1056 -324
  25. package/dist/extensions/pi-powerline-footer/lifecycle.ts +10 -0
  26. package/dist/extensions/pi-powerline-footer/package.json +10 -6
  27. package/dist/extensions/pi-powerline-footer/paths.ts +22 -0
  28. package/dist/extensions/pi-powerline-footer/powerline-config.ts +297 -15
  29. package/dist/extensions/pi-powerline-footer/presets.ts +6 -25
  30. package/dist/extensions/pi-powerline-footer/queue/store.ts +361 -0
  31. package/dist/extensions/pi-powerline-footer/queue/types.ts +54 -0
  32. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +12 -7
  33. package/dist/extensions/pi-powerline-footer/segments.ts +114 -107
  34. package/dist/extensions/pi-powerline-footer/session-usage.ts +1 -1
  35. package/dist/extensions/pi-powerline-footer/shortcuts.ts +17 -3
  36. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +189 -17
  37. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +144 -0
  38. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +120 -2
  39. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +245 -22
  40. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +48 -0
  41. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +34 -0
  42. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +39 -0
  43. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +92 -0
  44. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +11 -6
  45. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +33 -197
  46. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +79 -0
  47. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +63 -0
  48. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +195 -0
  49. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +14 -0
  50. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +167 -0
  51. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +23 -18
  52. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +16 -1
  53. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +249 -0
  54. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +237 -0
  55. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +199 -0
  56. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +191 -100
  57. package/dist/extensions/pi-powerline-footer/theme.ts +32 -18
  58. package/dist/extensions/pi-powerline-footer/token-stats.ts +304 -0
  59. package/dist/extensions/pi-powerline-footer/tsconfig.json +13 -0
  60. package/dist/extensions/pi-powerline-footer/types.ts +74 -29
  61. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +34 -0
  62. package/dist/extensions/pi-powerline-footer/welcome.ts +65 -10
  63. package/dist/extensions/pi-powerline-footer/working-vibes.ts +44 -11
  64. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  65. package/dist/extensions/pi-subagents/package.json +1 -1
  66. package/dist/extensions/pi-subagents/src/slash/inline-subagents.ts +57 -24
  67. package/dist/extensions/pi-subagents/test/unit/inline-subagents.test.ts +82 -7
  68. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +1 -1
  69. package/dist/extensions/ponytail/index.js +1 -31
  70. package/dist/extensions/ponytail/test/extension.test.js +5 -22
  71. package/dist/extensions/rtk.test.ts +27 -12
  72. package/dist/extensions/rtk.ts +41 -35
  73. package/package.json +1 -1
@@ -9,11 +9,11 @@ import {
9
9
  type Theme,
10
10
  } from "@selesai/code";
11
11
  import type { AssistantMessage } from "@earendil-works/pi-ai";
12
- import { isKeyRelease, matchesKey, type AutocompleteProvider, type SelectItem, SelectList, truncateToWidth, TUI_KEYBINDINGS, visibleWidth } from "@earendil-works/pi-tui";
12
+ import { isKeyRelease, type AutocompleteProvider, type SelectItem, SelectList, truncateToWidth, TUI_KEYBINDINGS, visibleWidth } from "@earendil-works/pi-tui";
13
13
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
14
14
  import { join, dirname } from "node:path";
15
15
 
16
- import type { ColorScheme, SegmentContext, StatusLinePreset, StatusLineSegmentId } from "./types.ts";
16
+ import type { ColorScheme, SegmentContext, StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "./types.ts";
17
17
  import type { PowerlineConfig } from "./powerline-config.ts";
18
18
  import { BashTranscriptStore } from "./bash-mode/transcript.ts";
19
19
  import {
@@ -28,21 +28,26 @@ import { ManagedShellSession } from "./bash-mode/shell-session.ts";
28
28
  import { matchHistoryEntries, readGlobalShellHistory, readProjectHistory, appendProjectHistory } from "./bash-mode/history.ts";
29
29
  import type { BashModeSettings } from "./bash-mode/types.ts";
30
30
  import { getPreset, PRESETS } from "./presets.ts";
31
- import { collectHiddenExtensionStatusKeys, getNotificationExtensionStatuses, mergeSegmentsWithCustomItems, nextPowerlineSettingWithOptions, nextPowerlineSettingWithPreset, parsePowerlineConfig } from "./powerline-config.ts";
31
+ import { getAgentPath } from "./paths.ts";
32
+ import { collectHiddenExtensionStatusKeys, getNotificationExtensionStatuses, mergeSegmentOptions, mergeSegmentsWithCustomItems, nextPowerlineSettingWithOptions, nextPowerlineSettingWithPreset, parsePowerlineConfig } from "./powerline-config.ts";
32
33
  import { getSeparator } from "./separators.ts";
33
34
  import { renderSegment } from "./segments.ts";
34
- import { getGitStatus, invalidateGitStatus, invalidateGitBranch } from "./git-status.ts";
35
+ import { getGitStatus, invalidateGitStatus, invalidateGitBranch, subscribeGitUpdates } from "./git-status.ts";
36
+ import { SessionBranchCache, SessionTokenStatsCache } from "./token-stats.ts";
35
37
  import { ansi, getFgAnsiCode } from "./colors.ts";
36
38
  import { WelcomeComponent, WelcomeHeader, discoverLoadedCounts, getRecentSessions } from "./welcome.ts";
37
39
  import { getGuidePreferences, getNewGuideFeatures, markGuideVersionSeen, resolveGuideDisplayMode, saveGuidePreferences, type GuideFeature } from "./guide.ts";
38
40
  import { createRenderScheduler } from "./render-scheduler.ts";
39
- import { readCoreContextUsage } from "./context-usage.ts";
40
- import { renderFixedEditorCluster } from "./fixed-editor/cluster.ts";
41
- import { emergencyTerminalModeReset, TerminalSplitCompositor } from "./fixed-editor/terminal-split.ts";
41
+ import { createWelcomeDismissScheduler } from "./welcome-dismiss.ts";
42
+ import { getEditorAutocompleteProvider, passAutocompleteProviderThroughPreviousEditor } from "./editor-composition.ts";
43
+ import { CoreContextUsageCache, estimateInitialContextTokens, estimateUnknownContextUsage, resolveDisplayContextUsage, type CoreContextUsage } from "./context-usage.ts";
44
+ import { isStaleExtensionContextError, shouldShowStartupWelcome } from "./lifecycle.ts";
42
45
  import { getDefaultColors } from "./theme.ts";
46
+ import { registerCdCommand } from "./cd-command.ts";
43
47
  import {
44
48
  isSupportedSuperShortcut,
45
49
  matchesConfiguredShortcut,
50
+ matchesStashShortcutInput,
46
51
  shortcutConflictKey,
47
52
  shortcutUsesSuper,
48
53
  } from "./shortcuts.ts";
@@ -61,9 +66,14 @@ import {
61
66
  hasVibeFile,
62
67
  getVibeFileCount,
63
68
  generateVibesBatch,
69
+ parseVibeGenerateArgs,
64
70
  } from "./working-vibes.ts";
65
71
  import { setupTpsTracker } from "./tps.ts";
66
- import { readAsyncSubagentUsage, readSubagentToolResultUsage } from "./session-usage.ts";
72
+ import { readAsyncSubagentUsage } from "./session-usage.ts";
73
+ import { renderFixedEditorCluster } from "./fixed-editor/cluster.ts";
74
+ import { emergencyTerminalModeReset, TerminalSplitCompositor } from "./fixed-editor/terminal-split.ts";
75
+ import { PowerlineQueueStore, currentQueueContext, formatIdeaIssuePrompt, formatQueueDeliveryText, parseCompactQueuedPrompt, parseSigilIdeaCapture, parseTargetPrefix, targetForIdea } from "./queue/store.ts";
76
+ import type { PowerlineQueueItem, QueueContext, QueueIntent, QueueTarget } from "./queue/types.ts";
67
77
 
68
78
  // ═══════════════════════════════════════════════════════════════════════════
69
79
  // Configuration
@@ -75,24 +85,39 @@ let config: PowerlineConfig = {
75
85
  mouseScroll: true,
76
86
  fixedEditor: true,
77
87
  fixedEditorPromptGlyph: "",
88
+ disabledSegments: [],
89
+ invalidDisabledSegments: [],
90
+ layout: null,
91
+ invalidLayoutSegments: [],
92
+ separator: null,
93
+ segmentOptions: {},
94
+ placement: "above",
95
+ invalidPlacement: null,
96
+ welcome: true,
97
+ stashSharpSShortcut: false,
98
+ queue: { captureSigil: "#" },
78
99
  };
79
100
 
80
101
  const CUSTOM_COMPACTION_STATUS_KEY = "compact-policy";
81
102
  let customCompactionEnabled = false;
82
103
 
83
- interface PowerlineShortcuts {
84
- stashHistory: string;
85
- copyEditor: string;
86
- cutEditor: string;
87
- jumpPreviousUserMessage: string;
88
- jumpNextUserMessage: string;
89
- jumpPreviousLlmMessage: string;
90
- jumpNextLlmMessage: string;
91
- jumpChatBottom: string;
92
- scrollChatUp: string;
93
- scrollChatDown: string;
94
- editorStart: string;
95
- editorEnd: string;
104
+ type ShortcutBinding = string | null;
105
+
106
+ export interface PowerlineShortcuts {
107
+ stashHistory: ShortcutBinding;
108
+ copyEditor: ShortcutBinding;
109
+ cutEditor: ShortcutBinding;
110
+ ideaCapture: ShortcutBinding;
111
+ queueOpen: ShortcutBinding;
112
+ jumpPreviousUserMessage: ShortcutBinding;
113
+ jumpNextUserMessage: ShortcutBinding;
114
+ jumpPreviousLlmMessage: ShortcutBinding;
115
+ jumpNextLlmMessage: ShortcutBinding;
116
+ jumpChatBottom: ShortcutBinding;
117
+ scrollChatUp: ShortcutBinding;
118
+ scrollChatDown: ShortcutBinding;
119
+ editorStart: ShortcutBinding;
120
+ editorEnd: ShortcutBinding;
96
121
  }
97
122
 
98
123
  type PowerlineShortcutKey = keyof PowerlineShortcuts;
@@ -112,9 +137,10 @@ type PowerlineShortcutAction =
112
137
  | { kind: "stashHistory" }
113
138
  | { kind: "copyEditor" }
114
139
  | { kind: "cutEditor" }
140
+ | { kind: "ideaCapture" }
141
+ | { kind: "queueOpen" }
115
142
  | { kind: "bashMode" }
116
143
  | { kind: "chat"; action: ChatJumpShortcutAction };
117
-
118
144
  const STASH_HISTORY_LIMIT = 12;
119
145
  const PROJECT_PROMPT_HISTORY_LIMIT = 50;
120
146
  const STASH_PREVIEW_WIDTH = 72;
@@ -122,6 +148,8 @@ const DEFAULT_SHORTCUTS: PowerlineShortcuts = {
122
148
  stashHistory: "ctrl+alt+h",
123
149
  copyEditor: "ctrl+alt+c",
124
150
  cutEditor: "ctrl+alt+x",
151
+ ideaCapture: null,
152
+ queueOpen: "ctrl+alt+q",
125
153
  jumpPreviousUserMessage: "ctrl+shift+u",
126
154
  jumpNextUserMessage: "ctrl+shift+i",
127
155
  jumpPreviousLlmMessage: "ctrl+alt+,",
@@ -132,11 +160,12 @@ const DEFAULT_SHORTCUTS: PowerlineShortcuts = {
132
160
  editorStart: "super+shift+up",
133
161
  editorEnd: "super+shift+down",
134
162
  };
135
- const DEFAULT_BASH_MODE_SETTINGS: BashModeSettings = {
163
+ const DEFAULT_BASH_MODE_SETTINGS = {
136
164
  toggleShortcut: "ctrl+shift+b",
137
165
  transcriptMaxLines: 2000,
138
166
  transcriptMaxBytes: 512 * 1024,
139
- };
167
+ } as const satisfies BashModeSettings;
168
+ const SHORTCUT_KEYS: PowerlineShortcutKey[] = ["stashHistory", "copyEditor", "cutEditor", "ideaCapture", "queueOpen", "jumpPreviousUserMessage", "jumpNextUserMessage", "jumpPreviousLlmMessage", "jumpNextLlmMessage", "jumpChatBottom", "scrollChatUp", "scrollChatDown", "editorStart", "editorEnd"];
140
169
  const CHAT_JUMP_SHORTCUTS: Array<{
141
170
  shortcutKey: ChatJumpShortcutKey;
142
171
  description: string;
@@ -168,20 +197,6 @@ const CHAT_JUMP_SHORTCUTS: Array<{
168
197
  action: { kind: "bottom" },
169
198
  },
170
199
  ];
171
- const SHORTCUT_KEYS: PowerlineShortcutKey[] = [
172
- "stashHistory",
173
- "copyEditor",
174
- "cutEditor",
175
- "jumpPreviousUserMessage",
176
- "jumpNextUserMessage",
177
- "jumpPreviousLlmMessage",
178
- "jumpNextLlmMessage",
179
- "jumpChatBottom",
180
- "scrollChatUp",
181
- "scrollChatDown",
182
- "editorStart",
183
- "editorEnd",
184
- ];
185
200
  const APP_RESERVED_SHORTCUTS = [
186
201
  "escape",
187
202
  "ctrl+c",
@@ -212,7 +227,7 @@ const APP_RESERVED_SHORTCUTS = [
212
227
  ] as const;
213
228
  const EXTRA_RESERVED_SHORTCUTS = ["alt+s"] as const;
214
229
  const SHORTCUT_MODIFIER_ORDER = ["ctrl", "alt", "super", "shift"] as const;
215
- const SHORTCUT_MODIFIERS = new Set(SHORTCUT_MODIFIER_ORDER);
230
+ const SHORTCUT_MODIFIERS = new Set<string>(SHORTCUT_MODIFIER_ORDER);
216
231
  const SHORTCUT_NAMED_KEYS = new Set([
217
232
  "escape", "esc", "enter", "return", "tab", "space", "backspace", "delete", "insert", "clear",
218
233
  "home", "end", "pageup", "pagedown", "up", "down", "left", "right",
@@ -230,6 +245,10 @@ const EDITOR_STATUS_DEFER_MS = 150;
230
245
  const PROMPT_HISTORY_TRACKED = Symbol.for("powerlinePromptHistoryTracked");
231
246
  const PROMPT_HISTORY_STATE_KEY = Symbol.for("powerlinePromptHistoryState");
232
247
 
248
+ interface PromptHistoryEditor {
249
+ addToHistory?: (text: string) => void;
250
+ }
251
+
233
252
  type PromptHistoryState = { savedPromptHistory: string[] };
234
253
  type SessionAssistantUsage = AssistantMessage["usage"];
235
254
 
@@ -279,8 +298,9 @@ function getPromptHistoryState(): PromptHistoryState {
279
298
  return state;
280
299
  }
281
300
 
282
- function readPromptHistory(editor: any): string[] {
283
- const history = editor?.history;
301
+ function readPromptHistory(editor: PromptHistoryEditor | null | undefined): string[] {
302
+ if (!editor) return [];
303
+ const history = Reflect.get(editor, "history");
284
304
  if (!Array.isArray(history)) return [];
285
305
 
286
306
  const normalized: string[] = [];
@@ -296,14 +316,14 @@ function readPromptHistory(editor: any): string[] {
296
316
  return normalized;
297
317
  }
298
318
 
299
- function snapshotPromptHistory(editor: any): void {
319
+ function snapshotPromptHistory(editor: PromptHistoryEditor | null | undefined): void {
300
320
  const history = readPromptHistory(editor);
301
321
  if (history.length > 0) {
302
322
  getPromptHistoryState().savedPromptHistory = [...history];
303
323
  }
304
324
  }
305
325
 
306
- function restorePromptHistory(editor: any): void {
326
+ function restorePromptHistory(editor: PromptHistoryEditor | null | undefined): void {
307
327
  const { savedPromptHistory } = getPromptHistoryState();
308
328
  if (!savedPromptHistory.length || typeof editor?.addToHistory !== "function") return;
309
329
 
@@ -312,9 +332,9 @@ function restorePromptHistory(editor: any): void {
312
332
  }
313
333
  }
314
334
 
315
- function trackPromptHistory(editor: any): void {
335
+ function trackPromptHistory(editor: PromptHistoryEditor | null | undefined): void {
316
336
  if (!editor || typeof editor.addToHistory !== "function") return;
317
- if (editor[PROMPT_HISTORY_TRACKED]) {
337
+ if (Reflect.get(editor, PROMPT_HISTORY_TRACKED)) {
318
338
  snapshotPromptHistory(editor);
319
339
  return;
320
340
  }
@@ -324,7 +344,7 @@ function trackPromptHistory(editor: any): void {
324
344
  originalAddToHistory(text);
325
345
  snapshotPromptHistory(editor);
326
346
  };
327
- editor[PROMPT_HISTORY_TRACKED] = true;
347
+ Reflect.set(editor, PROMPT_HISTORY_TRACKED, true);
328
348
  snapshotPromptHistory(editor);
329
349
  }
330
350
 
@@ -624,7 +644,7 @@ function writePowerlinePresetSetting(preset: StatusLinePreset, cwd: string = pro
624
644
 
625
645
  function writePowerlineOptionSetting(
626
646
  cwd: string,
627
- updates: Partial<Pick<PowerlineConfig, "mouseScroll" | "fixedEditor">>,
647
+ updates: Partial<Pick<PowerlineConfig, "welcome" | "stashSharpSShortcut" | "placement">>,
628
648
  currentPreset: StatusLinePreset,
629
649
  ): boolean {
630
650
  return writePowerlineSetting(cwd, (existingPowerlineSetting) => (
@@ -652,7 +672,9 @@ function hasNonWhitespaceText(text: string): boolean {
652
672
  }
653
673
 
654
674
  function getCurrentEditorText(ctx: any, editor: any): string {
655
- return editor?.getExpandedText?.() ?? ctx.ui.getEditorText();
675
+ const editorText = editor?.getExpandedText?.();
676
+ if (typeof editorText === "string" && editorText.length > 0) return editorText;
677
+ return ctx.ui.getEditorText?.() ?? editorText ?? "";
656
678
  }
657
679
 
658
680
  function buildStashPreview(text: string, maxWidth: number): string {
@@ -677,7 +699,7 @@ function normalizeShortcut(value: string): string {
677
699
  const parts = value.trim().toLowerCase().split("+");
678
700
  if (parts.length <= 1) return parts[0] ?? "";
679
701
 
680
- const modifierRank = new Map(SHORTCUT_MODIFIER_ORDER.map((modifier, index) => [modifier, index]));
702
+ const modifierRank = new Map<string, number>(SHORTCUT_MODIFIER_ORDER.map((modifier, index) => [modifier, index]));
681
703
  const modifiers = parts.slice(0, -1).sort((a, b) => (modifierRank.get(a) ?? 99) - (modifierRank.get(b) ?? 99));
682
704
  return [...modifiers, parts[parts.length - 1]].join("+");
683
705
  }
@@ -756,15 +778,21 @@ function shortcutUsageKey(shortcut: string): string {
756
778
  return shortcutConflictKey(normalizeShortcut(shortcut));
757
779
  }
758
780
 
781
+ function parseShortcutSetting(value: unknown): ShortcutBinding | undefined {
782
+ if (value === null || value === undefined) return null;
783
+ if (typeof value === "string" && value.trim() === "") return null;
784
+ return parseShortcutOverride(value) ?? undefined;
785
+ }
786
+
759
787
  function findShortcutReplacement(key: PowerlineShortcutKey, used: Set<string>): string | null {
760
788
  const preferred = DEFAULT_SHORTCUTS[key];
761
- if (!used.has(shortcutUsageKey(preferred))) {
789
+ if (preferred && !used.has(shortcutUsageKey(preferred))) {
762
790
  return preferred;
763
791
  }
764
792
 
765
793
  for (const shortcutKey of SHORTCUT_KEYS) {
766
794
  const candidate = DEFAULT_SHORTCUTS[shortcutKey];
767
- if (!used.has(shortcutUsageKey(candidate))) {
795
+ if (candidate && !used.has(shortcutUsageKey(candidate))) {
768
796
  return candidate;
769
797
  }
770
798
  }
@@ -772,23 +800,45 @@ function findShortcutReplacement(key: PowerlineShortcutKey, used: Set<string>):
772
800
  return null;
773
801
  }
774
802
 
775
- function resolveShortcutConfig(settings: Record<string, unknown>): PowerlineShortcuts {
803
+ function bashToggleShortcutReservation(settings: Record<string, unknown>): ShortcutBinding {
804
+ const raw = isRecord(settings.bashMode) ? settings.bashMode : {};
805
+ if (!Object.prototype.hasOwnProperty.call(raw, "toggleShortcut")) {
806
+ return DEFAULT_BASH_MODE_SETTINGS.toggleShortcut;
807
+ }
808
+
809
+ const parsed = parseShortcutSetting(raw.toggleShortcut);
810
+ return parsed === undefined ? DEFAULT_BASH_MODE_SETTINGS.toggleShortcut : parsed;
811
+ }
812
+
813
+ export function resolveShortcutConfig(settings: Record<string, unknown>): PowerlineShortcuts {
776
814
  const resolved: PowerlineShortcuts = { ...DEFAULT_SHORTCUTS };
777
815
  const shortcutSettings = settings.powerlineShortcuts;
778
816
 
779
817
  if (isRecord(shortcutSettings)) {
780
818
  for (const key of SHORTCUT_KEYS) {
781
- const override = parseShortcutOverride(shortcutSettings[key]);
782
- if (override) {
819
+ if (!Object.prototype.hasOwnProperty.call(shortcutSettings, key)) {
820
+ continue;
821
+ }
822
+
823
+ const override = parseShortcutSetting(shortcutSettings[key]);
824
+ if (override !== undefined) {
783
825
  resolved[key] = override;
784
826
  }
785
827
  }
786
828
  }
787
829
 
788
830
  const used = new Set(Array.from(reservedShortcuts(), shortcutUsageKey));
831
+ const reservedBashToggle = bashToggleShortcutReservation(settings);
832
+ if (reservedBashToggle) {
833
+ used.add(shortcutUsageKey(reservedBashToggle));
834
+ }
789
835
 
790
836
  for (const key of SHORTCUT_KEYS) {
791
837
  const configured = resolved[key];
838
+ if (configured === null) {
839
+ continue;
840
+ }
841
+
792
842
  const configuredUsageKey = shortcutUsageKey(configured);
793
843
 
794
844
  if (!used.has(configuredUsageKey)) {
@@ -813,17 +863,33 @@ function resolveShortcutConfig(settings: Record<string, unknown>): PowerlineShor
813
863
  return resolved;
814
864
  }
815
865
 
816
- function parseBashModeSettings(settings: Record<string, unknown>): BashModeSettings {
866
+ export function parseBashModeSettings(settings: Record<string, unknown>, powerlineShortcuts?: PowerlineShortcuts): BashModeSettings {
817
867
  const raw = isRecord(settings.bashMode) ? settings.bashMode : {};
868
+ const used = new Set(Array.from(reservedShortcuts(), shortcutUsageKey));
869
+ if (powerlineShortcuts) {
870
+ for (const shortcut of Object.values(powerlineShortcuts)) {
871
+ if (shortcut) {
872
+ used.add(shortcutUsageKey(shortcut));
873
+ }
874
+ }
875
+ }
818
876
 
819
- const configuredToggleShortcut = parseShortcutOverride(raw.toggleShortcut);
820
- const toggleShortcut = configuredToggleShortcut && !reservedShortcuts().has(shortcutUsageKey(configuredToggleShortcut))
821
- ? configuredToggleShortcut
877
+ const configuredToggleShortcut = Object.prototype.hasOwnProperty.call(raw, "toggleShortcut")
878
+ ? parseShortcutSetting(raw.toggleShortcut)
879
+ : undefined;
880
+ const fallbackToggleShortcut = used.has(shortcutUsageKey(DEFAULT_BASH_MODE_SETTINGS.toggleShortcut))
881
+ ? null
822
882
  : DEFAULT_BASH_MODE_SETTINGS.toggleShortcut;
883
+ const toggleShortcut = configuredToggleShortcut === null
884
+ ? null
885
+ : configuredToggleShortcut
886
+ && !used.has(shortcutUsageKey(configuredToggleShortcut))
887
+ ? configuredToggleShortcut
888
+ : fallbackToggleShortcut;
823
889
 
824
890
  if (configuredToggleShortcut && toggleShortcut !== configuredToggleShortcut) {
825
891
  console.debug(
826
- `[powerline-footer] Bash mode shortcut conflict: "${configuredToggleShortcut}" replaced with "${toggleShortcut}"`,
892
+ `[powerline-footer] Bash mode shortcut conflict: "${configuredToggleShortcut}" replaced with "${toggleShortcut ?? "disabled"}"`,
827
893
  );
828
894
  }
829
895
  const transcriptMaxLines = typeof raw.transcriptMaxLines === "number" && Number.isFinite(raw.transcriptMaxLines)
@@ -859,10 +925,10 @@ function renderSegmentWithWidth(
859
925
  /** Build content string from pre-rendered parts */
860
926
  function buildContentFromParts(
861
927
  parts: string[],
862
- presetDef: ReturnType<typeof getPreset>
928
+ separatorStyle: StatusLineSeparatorStyle,
863
929
  ): string {
864
930
  if (parts.length === 0) return "";
865
- const separatorDef = getSeparator(presetDef.separator);
931
+ const separatorDef = getSeparator(separatorStyle);
866
932
  const sepAnsi = getFgAnsiCode("sep");
867
933
  const sep = separatorDef.left;
868
934
  return " " + parts.join(`${sepAnsi}${sep}${ansi.reset}`) + ansi.reset + " ";
@@ -878,37 +944,70 @@ function computeResponsiveLayout(
878
944
  presetDef: ReturnType<typeof getPreset>,
879
945
  availableWidth: number
880
946
  ): { topContent: string; secondaryContent: string } {
881
- const separatorDef = getSeparator(presetDef.separator);
882
- const sepWidth = visibleWidth(separatorDef.left); // separator, no surrounding spaces
947
+ const separatorStyle = config.separator ?? presetDef.separator;
948
+ const separatorDef = getSeparator(separatorStyle);
949
+ const sepWidth = visibleWidth(separatorDef.left) + 2; // separator + spaces around it
950
+
951
+ // Get all segments: primary first, then secondary
952
+ const mergedSegments = mergeSegmentsWithCustomItems(presetDef, config.customItems, {
953
+ layout: config.layout,
954
+ disabledSegments: config.disabledSegments,
955
+ });
956
+ const primaryIds = [...mergedSegments.leftSegments, ...mergedSegments.rightSegments];
957
+ const secondaryIds = mergedSegments.secondarySegments;
958
+ const allSegmentIds = [...primaryIds, ...secondaryIds];
883
959
 
884
- const mergedSegments = mergeSegmentsWithCustomItems(presetDef, config.customItems);
885
- const renderIds = (ids: StatusLineSegmentId[]): string[] => {
886
- const parts: string[] = [];
887
- for (const segId of ids) {
888
- const { content, visible } = renderSegmentWithWidth(segId, ctx);
889
- if (visible) parts.push(content);
960
+ // Render all segments and get their widths
961
+ const renderedSegments: { content: string; width: number }[] = [];
962
+ for (const segId of allSegmentIds) {
963
+ const { content, width, visible } = renderSegmentWithWidth(segId, ctx);
964
+ if (visible) {
965
+ renderedSegments.push({ content, width });
890
966
  }
891
- return parts;
892
- };
967
+ }
968
+
969
+ if (renderedSegments.length === 0) {
970
+ return { topContent: "", secondaryContent: "" };
971
+ }
893
972
 
894
- const leftParts = renderIds(mergedSegments.leftSegments);
895
- const rightParts = renderIds(mergedSegments.rightSegments);
896
- const secondaryParts = renderIds(mergedSegments.secondarySegments);
973
+ // Calculate how many segments fit in top bar
974
+ // Account for: leading space (1) + trailing space (1) = 2 chars overhead
975
+ const baseOverhead = 2;
976
+ let currentWidth = baseOverhead;
977
+ let topSegments: string[] = [];
978
+ let overflowSegments: { content: string; width: number }[] = [];
979
+ let overflow = false;
980
+
981
+ for (const seg of renderedSegments) {
982
+ const neededWidth = seg.width + (topSegments.length > 0 ? sepWidth : 0);
983
+
984
+ if (!overflow && currentWidth + neededWidth <= availableWidth) {
985
+ topSegments.push(seg.content);
986
+ currentWidth += neededWidth;
987
+ } else {
988
+ overflow = true;
989
+ overflowSegments.push(seg);
990
+ }
991
+ }
897
992
 
898
- const leftContent = buildContentFromParts(leftParts, presetDef);
899
- const rightContent = buildContentFromParts(rightParts, presetDef);
900
- let topContent = leftContent;
993
+ // Fit overflow segments into secondary row (same width constraint)
994
+ // Stop at first non-fitting segment to preserve ordering
995
+ let secondaryWidth = baseOverhead;
996
+ let secondarySegments: string[] = [];
901
997
 
902
- if (rightContent) {
903
- const gap = Math.max(0, availableWidth - visibleWidth(leftContent) - visibleWidth(rightContent));
904
- topContent = leftContent
905
- ? `${leftContent}${" ".repeat(gap)}${rightContent}`
906
- : `${" ".repeat(gap)}${rightContent}`;
998
+ for (const seg of overflowSegments) {
999
+ const neededWidth = seg.width + (secondarySegments.length > 0 ? sepWidth : 0);
1000
+ if (secondaryWidth + neededWidth <= availableWidth) {
1001
+ secondarySegments.push(seg.content);
1002
+ secondaryWidth += neededWidth;
1003
+ } else {
1004
+ break;
1005
+ }
907
1006
  }
908
1007
 
909
1008
  return {
910
- topContent,
911
- secondaryContent: buildContentFromParts(secondaryParts, presetDef),
1009
+ topContent: buildContentFromParts(topSegments, separatorStyle),
1010
+ secondaryContent: buildContentFromParts(secondarySegments, separatorStyle),
912
1011
  };
913
1012
  }
914
1013
 
@@ -916,11 +1015,33 @@ function computeResponsiveLayout(
916
1015
  // Extension
917
1016
  // ═══════════════════════════════════════════════════════════════════════════
918
1017
 
1018
+ function warnInvalidSegmentSettings(ctx: any): void {
1019
+ if (config.invalidDisabledSegments.length > 0) {
1020
+ const invalid = config.invalidDisabledSegments.map((id) => JSON.stringify(id)).join(", ");
1021
+ const message = `Ignoring unknown powerline disabled segment${config.invalidDisabledSegments.length === 1 ? "" : "s"}: ${invalid}`;
1022
+ console.warn(`[powerline-footer] ${message}`);
1023
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
1024
+ }
1025
+
1026
+ if (config.invalidLayoutSegments.length > 0) {
1027
+ const invalid = config.invalidLayoutSegments.map((id) => JSON.stringify(id)).join(", ");
1028
+ const message = `Ignoring unknown powerline layout segment${config.invalidLayoutSegments.length === 1 ? "" : "s"}: ${invalid}`;
1029
+ console.warn(`[powerline-footer] ${message}`);
1030
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
1031
+ }
1032
+
1033
+ if (config.invalidPlacement !== null) {
1034
+ const message = `Ignoring invalid powerline placement: ${JSON.stringify(config.invalidPlacement)}`;
1035
+ console.warn(`[powerline-footer] ${message}`);
1036
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
1037
+ }
1038
+ }
1039
+
919
1040
  export default function powerlineFooter(pi: ExtensionAPI) {
920
1041
  const startupSettings = readSettings();
921
1042
  config = parsePowerlineConfig(startupSettings.powerline, PRESET_NAMES);
922
1043
  let resolvedShortcuts = resolveShortcutConfig(startupSettings);
923
- let bashModeSettings = parseBashModeSettings(startupSettings);
1044
+ let bashModeSettings = parseBashModeSettings(startupSettings, resolvedShortcuts);
924
1045
 
925
1046
  let enabled = true;
926
1047
  let sessionStartTime = Date.now();
@@ -931,14 +1052,10 @@ export default function powerlineFooter(pi: ExtensionAPI) {
931
1052
  let currentThinkingLevel: string | null = null;
932
1053
  let liveAssistantUsage: SessionAssistantUsage | null = null;
933
1054
  const completedAsyncSubagentUsage = new Map<string, { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }>();
1055
+ let approximateContextUsage: CoreContextUsage | null = null;
934
1056
  let isStreaming = false;
935
1057
  let tuiRef: any = null;
936
1058
  let restoreFooterStatusRepaintHook: (() => void) | null = null;
937
- let fixedEditorCompositor: TerminalSplitCompositor | null = null;
938
- let fixedStatusContainer: any = null;
939
- let fixedEditorContainer: any = null;
940
- let fixedWidgetContainerAbove: any = null;
941
- let fixedWidgetContainerBelow: any = null;
942
1059
  let stashShortcutInputUnsubscribe: (() => void) | null = null;
943
1060
  let dismissWelcomeOverlay: (() => void) | null = null;
944
1061
  let guideOverlayBlocking = false;
@@ -953,6 +1070,15 @@ export default function powerlineFooter(pi: ExtensionAPI) {
953
1070
  let bashTranscript = new BashTranscriptStore(bashModeSettings);
954
1071
  let bashCompletionEngine = new BashCompletionEngine();
955
1072
  let shellSession: ManagedShellSession | null = null;
1073
+ let fixedEditorCompositor: TerminalSplitCompositor | null = null;
1074
+ let fixedStatusContainer: any = null;
1075
+ let fixedEditorContainer: any = null;
1076
+ let fixedWidgetContainerAbove: any = null;
1077
+ let fixedWidgetContainerBelow: any = null;
1078
+ const queueStore = new PowerlineQueueStore();
1079
+ let powerlineCompacting = false;
1080
+ let deliverAfterRetrySettles = false;
1081
+ let queueDeliveryTimer: ReturnType<typeof setTimeout> | null = null;
956
1082
 
957
1083
  // Cache for the top and secondary powerline widgets.
958
1084
  let lastLayoutWidth = 0;
@@ -963,6 +1089,13 @@ export default function powerlineFooter(pi: ExtensionAPI) {
963
1089
  let lastEditorInputAt = 0;
964
1090
  let lastLayoutSessionName: string | undefined;
965
1091
 
1092
+ // Cache for token counting: avoid re-scanning the full session event list
1093
+ // on every render (250ms-1s cadence). Revalidates the trailing event's
1094
+ // stats signature so in-place streaming updates are not served stale.
1095
+ const sessionBranchCache = new SessionBranchCache();
1096
+ const tokenStatsCache = new SessionTokenStatsCache();
1097
+ const coreContextUsageCache = new CoreContextUsageCache();
1098
+
966
1099
  const getShellPath = () => process.env.SHELL || "/bin/sh";
967
1100
  const getShellCwd = () => shellSession?.state.cwd ?? currentCtx?.cwd ?? process.cwd();
968
1101
 
@@ -976,10 +1109,19 @@ export default function powerlineFooter(pi: ExtensionAPI) {
976
1109
  tuiRef?.requestRender();
977
1110
  }, STATUS_RENDER_DEBOUNCE_MS);
978
1111
 
1112
+ const welcomeDismissScheduler = createWelcomeDismissScheduler({
1113
+ dismiss: (ctx: unknown) => dismissWelcome(ctx),
1114
+ getGeneration: () => sessionGeneration,
1115
+ isEnabled: () => enabled,
1116
+ });
1117
+
979
1118
  const resetLayoutCache = () => {
980
1119
  lastLayoutResult = null;
981
1120
  lastLayoutSessionName = undefined;
982
1121
  layoutDirty = true;
1122
+ sessionBranchCache.reset();
1123
+ tokenStatsCache.reset();
1124
+ coreContextUsageCache.reset();
983
1125
  };
984
1126
 
985
1127
  const requestStatusRender = (delayMs?: number) => {
@@ -1120,7 +1262,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1120
1262
  items: SelectItem[],
1121
1263
  maxVisible: number,
1122
1264
  ): Promise<SelectItem | null> {
1123
- return ctx.ui.custom<SelectItem | null>(
1265
+ return ctx.ui.custom(
1124
1266
  (tui: any, theme: Theme, _keybindings: any, done: (result: SelectItem | null) => void) => {
1125
1267
  const selectList = new SelectList(items, maxVisible, overlaySelectListTheme(theme));
1126
1268
  const border = (text: string) => theme.fg("dim", text);
@@ -1167,6 +1309,291 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1167
1309
  );
1168
1310
  }
1169
1311
 
1312
+ function getQueueSessionId(ctx: any): string | undefined {
1313
+ const sessionId = ctx.sessionManager?.getSessionId?.();
1314
+ return typeof sessionId === "string" && sessionId.trim() ? sessionId : undefined;
1315
+ }
1316
+
1317
+ function getQueueContext(ctx: any): QueueContext {
1318
+ return currentQueueContext(ctx.cwd ?? process.cwd(), getQueueSessionId(ctx));
1319
+ }
1320
+
1321
+ function requestQueueRender(): void {
1322
+ requestImmediateStatusRender({ deferDuringTyping: false });
1323
+ }
1324
+
1325
+ function queueItemLabel(item: PowerlineQueueItem): string {
1326
+ const status = item.status === "queued" ? item.intent : `${item.intent}/${item.status}`;
1327
+ return `${item.id} ${status} ${buildStashPreview(item.text, 56)}`;
1328
+ }
1329
+
1330
+ function queueItemDescription(item: PowerlineQueueItem): string {
1331
+ if (item.target.kind === "global") return "global";
1332
+ if (item.target.kind === "current-session") return "current session";
1333
+ return item.target.alias ? `@${item.target.alias}` : item.target.cwd;
1334
+ }
1335
+
1336
+ function captureQueueItem(ctx: any, text: string, intent: QueueIntent, target: QueueTarget): PowerlineQueueItem {
1337
+ const item = queueStore.add({
1338
+ text,
1339
+ intent,
1340
+ target,
1341
+ source: getQueueContext(ctx),
1342
+ });
1343
+ requestQueueRender();
1344
+ return item;
1345
+ }
1346
+
1347
+ function captureIdeaFromParsedInput(ctx: any, parsed: { target: string | null; text: string }): PowerlineQueueItem | null {
1348
+ const trimmed = parsed.text.trim();
1349
+ if (!trimmed) {
1350
+ ctx.ui.notify("Nothing to capture", "info");
1351
+ return null;
1352
+ }
1353
+
1354
+ const target = targetForIdea(parsed.target, queueStore, ctx.cwd ?? process.cwd());
1355
+ const item = captureQueueItem(ctx, trimmed, "idea", target);
1356
+ ctx.ui.notify(`Idea saved (${item.id}) — /ideas to review`, "info");
1357
+ return item;
1358
+ }
1359
+
1360
+ function captureIdeaFromText(ctx: any, text: string): PowerlineQueueItem | null {
1361
+ return captureIdeaFromParsedInput(ctx, parseTargetPrefix(text));
1362
+ }
1363
+
1364
+ function captureCurrentProjectIdea(ctx: any, text: string): PowerlineQueueItem | null {
1365
+ const trimmed = text.trim();
1366
+ if (!trimmed) {
1367
+ ctx.ui.notify("Nothing to capture", "info");
1368
+ return null;
1369
+ }
1370
+
1371
+ const item = captureQueueItem(ctx, trimmed, "idea", { kind: "project", cwd: ctx.cwd ?? process.cwd() });
1372
+ ctx.ui.notify(`Idea saved (${item.id}) — /ideas to review`, "info");
1373
+ return item;
1374
+ }
1375
+
1376
+ function isSigilIdeaDraft(text: string): boolean {
1377
+ const sigil = config.queue.captureSigil;
1378
+ if (sigil === false) return false;
1379
+ const normalizedSigil = sigil.trim();
1380
+ if (!normalizedSigil) return false;
1381
+ const trimmed = text.trimStart();
1382
+ return trimmed.startsWith(normalizedSigil) && /^\s/.test(trimmed.slice(normalizedSigil.length));
1383
+ }
1384
+
1385
+ function captureSigilGlyph(): string {
1386
+ const sigil = config.queue.captureSigil === false ? "#" : config.queue.captureSigil;
1387
+ return Array.from(sigil)[0] ?? "#";
1388
+ }
1389
+
1390
+ function capturePostCompactPrompt(ctx: any, text: string): PowerlineQueueItem | null {
1391
+ const trimmed = text.trim();
1392
+ if (!trimmed) return null;
1393
+
1394
+ const item = captureQueueItem(ctx, trimmed, "post-compact", { kind: "current-session" });
1395
+ ctx.ui.notify(`Queued for after compaction (${item.id})`, "info");
1396
+ return item;
1397
+ }
1398
+
1399
+ function deliveryModeForItem(ctx: any, item: PowerlineQueueItem): "steer" | "followUp" | undefined {
1400
+ const idle = typeof ctx.isIdle === "function" ? ctx.isIdle() : true;
1401
+ if (idle) return undefined;
1402
+ return item.intent === "steer" ? "steer" : "followUp";
1403
+ }
1404
+
1405
+ function deliverQueueItem(ctx: any, item: PowerlineQueueItem): boolean {
1406
+ if (powerlineCompacting) {
1407
+ queueStore.update(item.id, { status: "queued" });
1408
+ requestQueueRender();
1409
+ return false;
1410
+ }
1411
+
1412
+ queueStore.update(item.id, { status: "delivering", error: undefined });
1413
+ requestQueueRender();
1414
+
1415
+ let sent = false;
1416
+ try {
1417
+ const deliverAs = deliveryModeForItem(ctx, item);
1418
+ const deliveryText = formatQueueDeliveryText(item);
1419
+ if (deliverAs) {
1420
+ pi.sendUserMessage(deliveryText, { deliverAs });
1421
+ } else {
1422
+ pi.sendUserMessage(deliveryText);
1423
+ }
1424
+ sent = true;
1425
+ queueStore.update(item.id, { status: "sent", error: undefined });
1426
+ try {
1427
+ ctx.ui.notify(`Sent queued item ${item.id}`, "info");
1428
+ } catch (error) {
1429
+ if (!isStaleExtensionContextError(error)) throw error;
1430
+ currentCtx = null;
1431
+ }
1432
+ requestQueueRender();
1433
+ return true;
1434
+ } catch (error) {
1435
+ if (isStaleExtensionContextError(error)) {
1436
+ if (!sent) queueStore.update(item.id, { status: "queued", error: undefined });
1437
+ currentCtx = null;
1438
+ throw error;
1439
+ }
1440
+ const message = error instanceof Error ? error.message : String(error);
1441
+ queueStore.update(item.id, { status: "failed", error: message });
1442
+ ctx.ui.notify(`Failed to send ${item.id}: ${message}`, "error");
1443
+ requestQueueRender();
1444
+ return false;
1445
+ }
1446
+ }
1447
+
1448
+ function schedulePostCompactionDelivery(ctx: any): void {
1449
+ if (queueDeliveryTimer) clearTimeout(queueDeliveryTimer);
1450
+ const queueContext = getQueueContext(ctx);
1451
+ const scheduledGeneration = sessionGeneration;
1452
+ queueDeliveryTimer = setTimeout(() => {
1453
+ queueDeliveryTimer = null;
1454
+ if (scheduledGeneration !== sessionGeneration) return;
1455
+ try {
1456
+ const item = queueStore.queuedDeliveryItems(queueContext, "post-compact")[0];
1457
+ if (item) deliverQueueItem(ctx, item);
1458
+ } catch (error) {
1459
+ if (!isStaleExtensionContextError(error)) throw error;
1460
+ currentCtx = null;
1461
+ }
1462
+ }, 50);
1463
+ }
1464
+
1465
+ function blockPostCompactionQueue(ctx: any, errorMessage: string): void {
1466
+ const items = queueStore.queuedDeliveryItems(getQueueContext(ctx), "post-compact");
1467
+ for (const item of items) {
1468
+ queueStore.update(item.id, { status: "blocked", error: errorMessage });
1469
+ }
1470
+ if (items.length > 0) {
1471
+ ctx.ui.notify(`Kept ${items.length} post-compaction item${items.length === 1 ? "" : "s"} blocked`, "warning");
1472
+ requestQueueRender();
1473
+ }
1474
+ }
1475
+
1476
+ function finishFailedCompaction(ctx: any, errorMessage: string): void {
1477
+ powerlineCompacting = false;
1478
+ deliverAfterRetrySettles = false;
1479
+ blockPostCompactionQueue(ctx, errorMessage);
1480
+ requestQueueRender();
1481
+ }
1482
+
1483
+ async function chooseQueueAction(ctx: any, item: PowerlineQueueItem): Promise<void> {
1484
+ const actions: SelectItem[] = [
1485
+ { value: "send", label: "Send to current session", description: "Deliver as prompt/follow-up" },
1486
+ { value: "edit", label: "Edit in prompt", description: "Move text into the editor" },
1487
+ { value: "retry", label: "Retry", description: "Mark queued and deliver" },
1488
+ { value: "clear", label: "Clear", description: "Mark item done" },
1489
+ { value: "cancel", label: "Cancel" },
1490
+ ];
1491
+ const selected = await showSelectOverlay(ctx, `Queue item ${item.id}`, buildStashPreview(item.text, 72), actions, actions.length);
1492
+ if (!selected || selected.value === "cancel") return;
1493
+
1494
+ if (selected.value === "send") {
1495
+ deliverQueueItem(ctx, item);
1496
+ return;
1497
+ }
1498
+
1499
+ if (selected.value === "retry") {
1500
+ const updated = queueStore.update(item.id, { status: "queued", error: undefined });
1501
+ if (updated) deliverQueueItem(ctx, updated);
1502
+ return;
1503
+ }
1504
+
1505
+ if (selected.value === "edit") {
1506
+ ctx.ui.setEditorText(item.text);
1507
+ queueStore.clear(item.id);
1508
+ requestQueueRender();
1509
+ return;
1510
+ }
1511
+
1512
+ if (selected.value === "clear") {
1513
+ queueStore.clear(item.id);
1514
+ ctx.ui.notify(`Cleared ${item.id}`, "info");
1515
+ requestQueueRender();
1516
+ }
1517
+ }
1518
+
1519
+ async function openQueuePicker(ctx: any, mode: "ideas" | "queue"): Promise<void> {
1520
+ const active = queueStore.activeItems(getQueueContext(ctx)).filter((item) => (
1521
+ mode === "ideas" ? item.intent === "idea" : item.intent !== "idea"
1522
+ ));
1523
+ if (active.length === 0) {
1524
+ ctx.ui.notify(mode === "ideas" ? "No ideas captured" : "No queued items", "info");
1525
+ return;
1526
+ }
1527
+
1528
+ const items: SelectItem[] = active.map((item) => ({
1529
+ value: item.id,
1530
+ label: queueItemLabel(item),
1531
+ description: queueItemDescription(item),
1532
+ }));
1533
+ const selected = await showSelectOverlay(
1534
+ ctx,
1535
+ mode === "ideas" ? "Powerline ideas" : "Powerline queue",
1536
+ "↑↓ navigate • enter manage • esc cancel",
1537
+ items,
1538
+ Math.min(active.length, 12),
1539
+ );
1540
+ if (!selected) return;
1541
+
1542
+ const item = active.find((candidate) => candidate.id === selected.value);
1543
+ if (item) await chooseQueueAction(ctx, item);
1544
+ }
1545
+
1546
+ function resolveCommandTarget(ctx: any, spec: string): QueueTarget {
1547
+ const normalized = spec.trim().replace(/^@/, "");
1548
+ return targetForIdea(normalized || null, queueStore, ctx.cwd ?? process.cwd());
1549
+ }
1550
+
1551
+ function sendOrRetryQueueItem(ctx: any, idPrefix: string): void {
1552
+ const item = queueStore.get(idPrefix);
1553
+ if (!item) {
1554
+ ctx.ui.notify(`No unique queue item matches ${idPrefix}`, "warning");
1555
+ return;
1556
+ }
1557
+ const updated = queueStore.update(item.id, { status: "queued", error: undefined });
1558
+ if (updated) deliverQueueItem(ctx, updated);
1559
+ }
1560
+
1561
+ function findNextIdea(ctx: any): PowerlineQueueItem | null {
1562
+ return queueStore.activeItems(getQueueContext(ctx)).find((candidate) => candidate.intent === "idea") ?? null;
1563
+ }
1564
+
1565
+ function sendIdeaIssueHandoff(ctx: any, item: PowerlineQueueItem): void {
1566
+ queueStore.update(item.id, { status: "delivering", error: undefined });
1567
+ requestQueueRender();
1568
+
1569
+ try {
1570
+ const deliverAs = deliveryModeForItem(ctx, item);
1571
+ const issuePrompt = formatIdeaIssuePrompt(item);
1572
+ if (deliverAs) {
1573
+ pi.sendUserMessage(issuePrompt, { deliverAs });
1574
+ } else {
1575
+ pi.sendUserMessage(issuePrompt);
1576
+ }
1577
+ queueStore.update(item.id, { status: "sent", error: undefined });
1578
+ ctx.ui.notify(`Sent idea ${item.id} for issue triage`, "info");
1579
+ requestQueueRender();
1580
+ } catch (error) {
1581
+ const message = error instanceof Error ? error.message : String(error);
1582
+ queueStore.update(item.id, { status: "failed", error: message });
1583
+ ctx.ui.notify(`Failed to send ${item.id}: ${message}`, "error");
1584
+ requestQueueRender();
1585
+ }
1586
+ }
1587
+
1588
+ function sendIdeaIssueHandoffById(ctx: any, id: string | undefined): void {
1589
+ const item = id ? queueStore.get(id) : findNextIdea(ctx);
1590
+ if (!item || item.intent !== "idea") {
1591
+ ctx.ui.notify(id ? `No unique idea matches ${id}` : "No ideas captured", id ? "warning" : "info");
1592
+ return;
1593
+ }
1594
+ sendIdeaIssueHandoff(ctx, item);
1595
+ }
1596
+
1170
1597
  // Track session start
1171
1598
  setupTpsTracker(pi);
1172
1599
 
@@ -1181,6 +1608,9 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1181
1608
  isStreaming = false;
1182
1609
  liveAssistantUsage = null;
1183
1610
  completedAsyncSubagentUsage.clear();
1611
+ approximateContextUsage = event.reason === "reload" ? estimateUnknownContextUsage(ctx) : null;
1612
+ powerlineCompacting = false;
1613
+ deliverAfterRetrySettles = false;
1184
1614
  stashedEditorText = null;
1185
1615
 
1186
1616
  const settings = readSettings(ctx.cwd);
@@ -1191,22 +1621,25 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1191
1621
  guideMode: guidePreferences.mode,
1192
1622
  });
1193
1623
  const newGuideFeatures = getNewGuideFeatures(guidePreferences.lastSeenVersion, VERSION);
1194
- bashModeSettings = parseBashModeSettings(settings);
1195
1624
  resolvedShortcuts = resolveShortcutConfig(settings);
1625
+ bashModeSettings = parseBashModeSettings(settings, resolvedShortcuts);
1196
1626
  showLastPrompt = settings.showLastPrompt !== false;
1197
1627
  config = parsePowerlineConfig(settings.powerline, PRESET_NAMES);
1628
+ warnInvalidSegmentSettings(ctx);
1198
1629
  stashedPromptHistory = readPersistedStashHistory();
1199
1630
  bashModeActive = false;
1200
1631
  bashTranscript = new BashTranscriptStore(bashModeSettings);
1201
1632
  bashCompletionEngine = new BashCompletionEngine();
1202
1633
 
1203
- getThinkingLevelFn = typeof ctx.getThinkingLevel === "function"
1204
- ? () => ctx.getThinkingLevel()
1205
- : null;
1206
- currentThinkingLevel = getThinkingLevelFn?.() ?? null;
1634
+ getThinkingLevelFn = () => ctx.thinkingLevel ?? "off";
1635
+ currentThinkingLevel = getThinkingLevelFn();
1207
1636
 
1208
1637
  if (ctx.hasUI) {
1209
1638
  ctx.ui.setStatus("stash", undefined);
1639
+ const pendingIdeas = queueStore.activeItems(getQueueContext(ctx)).filter((item) => item.intent === "idea").length;
1640
+ if (pendingIdeas > 0) {
1641
+ ctx.ui.notify(`${pendingIdeas} idea${pendingIdeas === 1 ? "" : "s"} waiting — /ideas`, "info");
1642
+ }
1210
1643
  }
1211
1644
 
1212
1645
  // Initialize vibe manager (needs modelRegistry from ctx)
@@ -1218,6 +1651,12 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1218
1651
  setupWelcomeOverlay(ctx, newGuideFeatures);
1219
1652
  } else if (guideDisplayMode === "compact") {
1220
1653
  setupWelcomeHeader(ctx, newGuideFeatures);
1654
+ } else if (shouldShowStartupWelcome(event.reason, config.welcome)) {
1655
+ if (settings.quietStartup === true) {
1656
+ setupWelcomeHeader(ctx, newGuideFeatures);
1657
+ } else {
1658
+ setupWelcomeOverlay(ctx, newGuideFeatures);
1659
+ }
1221
1660
  } else {
1222
1661
  dismissWelcome(ctx);
1223
1662
  }
@@ -1225,7 +1664,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1225
1664
 
1226
1665
  });
1227
1666
 
1228
- pi.on("session_shutdown", async () => {
1667
+ pi.on("session_shutdown", async (_event, ctx) => {
1229
1668
  sessionGeneration++;
1230
1669
  completedAsyncSubagentUsage.clear();
1231
1670
  dismissWelcomeOverlay?.();
@@ -1236,11 +1675,16 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1236
1675
  statusRenderScheduler.cancel();
1237
1676
  restoreFooterStatusRepaintHook?.();
1238
1677
  restoreFooterStatusRepaintHook = null;
1239
- teardownFixedEditorCompositor({ resetExtendedKeyboardModes: true });
1240
1678
  stashShortcutInputUnsubscribe?.();
1241
1679
  stashShortcutInputUnsubscribe = null;
1242
1680
  shellSession?.dispose();
1243
1681
  shellSession = null;
1682
+ if (queueDeliveryTimer) {
1683
+ clearTimeout(queueDeliveryTimer);
1684
+ queueDeliveryTimer = null;
1685
+ }
1686
+ powerlineCompacting = false;
1687
+ deliverAfterRetrySettles = false;
1244
1688
  bashModeActive = false;
1245
1689
  currentCtx = null;
1246
1690
  footerDataRef = null;
@@ -1280,16 +1724,16 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1280
1724
  }
1281
1725
  if (event.toolName === "write" || event.toolName === "edit") {
1282
1726
  invalidateGitStatus();
1727
+ requestStatusRender();
1283
1728
  }
1284
1729
  // Check for bash commands that might change git branch
1285
1730
  if (event.toolName === "bash" && event.input?.command) {
1286
1731
  const cmd = String(event.input.command);
1287
1732
  if (mightChangeGitBranch(cmd)) {
1288
- // Invalidate caches since working tree state changes with branch
1733
+ // The command has completed, so start refreshing immediately.
1289
1734
  invalidateGitStatus();
1290
1735
  invalidateGitBranch();
1291
- // Small delay to let git update, then re-render
1292
- setTimeout(() => requestStatusRender(), 100);
1736
+ requestStatusRender();
1293
1737
  }
1294
1738
  }
1295
1739
  });
@@ -1311,6 +1755,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1311
1755
 
1312
1756
  pi.on("model_select", async (_event, ctx) => {
1313
1757
  currentCtx = ctx;
1758
+ coreContextUsageCache.reset();
1314
1759
  requestStatusRender();
1315
1760
  });
1316
1761
 
@@ -1358,6 +1803,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1358
1803
 
1359
1804
  pi.on("message_end", async (event, ctx) => {
1360
1805
  currentCtx = ctx;
1806
+ coreContextUsageCache.reset();
1361
1807
  if (isSessionAssistantMessage(event.message)) {
1362
1808
  if (event.message.stopReason === "error" || event.message.stopReason === "aborted") {
1363
1809
  liveAssistantUsage = null;
@@ -1370,10 +1816,48 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1370
1816
 
1371
1817
  pi.on("turn_end", async (_event, ctx) => {
1372
1818
  currentCtx = ctx;
1819
+ coreContextUsageCache.reset();
1373
1820
  requestImmediateStatusRender({ deferDuringTyping: false });
1374
1821
  });
1375
1822
 
1376
- // Refresh vibe if rate limit allows. The guide is dismissed only by its input handler.
1823
+ pi.on("session_before_compact", async (_event, ctx) => {
1824
+ powerlineCompacting = true;
1825
+ currentCtx = ctx;
1826
+ isStreaming = false;
1827
+ liveAssistantUsage = null;
1828
+ approximateContextUsage = null;
1829
+ coreContextUsageCache.reset();
1830
+ requestQueueRender();
1831
+ });
1832
+
1833
+ pi.on("session_compact", async (event, ctx) => {
1834
+ powerlineCompacting = false;
1835
+ currentCtx = ctx;
1836
+ isStreaming = false;
1837
+ liveAssistantUsage = null;
1838
+ approximateContextUsage = estimateUnknownContextUsage(ctx);
1839
+ coreContextUsageCache.reset();
1840
+ if (event.willRetry) {
1841
+ deliverAfterRetrySettles = true;
1842
+ } else {
1843
+ deliverAfterRetrySettles = false;
1844
+ schedulePostCompactionDelivery(ctx);
1845
+ }
1846
+ requestQueueRender();
1847
+ });
1848
+
1849
+ pi.on("agent_settled", async (_event, ctx) => {
1850
+ if (powerlineCompacting) {
1851
+ finishFailedCompaction(ctx, "Compaction did not complete");
1852
+ return;
1853
+ }
1854
+ if (deliverAfterRetrySettles) {
1855
+ deliverAfterRetrySettles = false;
1856
+ schedulePostCompactionDelivery(ctx);
1857
+ }
1858
+ });
1859
+
1860
+ // Also dismiss on tool calls (agent is working) + refresh vibe if rate limit allows
1377
1861
  pi.on("tool_call", async (event, ctx) => {
1378
1862
  if (ctx.hasUI) {
1379
1863
  // Extract recent agent context from session for richer vibe generation
@@ -1424,6 +1908,11 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1424
1908
  }
1425
1909
  }
1426
1910
 
1911
+ function scheduleDismissWelcome(ctx: any) {
1912
+ if (!dismissWelcomeOverlay && welcomeOverlayShouldDismiss && !welcomeHeaderActive) return;
1913
+ welcomeDismissScheduler.schedule(ctx);
1914
+ }
1915
+
1427
1916
  function closeWelcome(ctx: any): void {
1428
1917
  dismissWelcomeOverlay?.();
1429
1918
  dismissWelcomeOverlay = null;
@@ -1559,20 +2048,25 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1559
2048
  }
1560
2049
  }
1561
2050
 
1562
- function isStashShortcutInput(data: string): boolean {
1563
- if (isKeyRelease(data)) return false;
2051
+ async function handleSelectedStashHistoryEntry(ctx: any, selected: string): Promise<void> {
2052
+ const action = await ctx.ui.select("Stashed prompt", ["Insert", "Promote to idea", "Cancel"]);
2053
+
2054
+ if (action === "Insert") {
2055
+ await insertSelectedPromptHistoryEntry(ctx, selected);
2056
+ return;
2057
+ }
1564
2058
 
1565
- return data === "ß"
1566
- || data === "\x1bs"
1567
- || data === "\x1bS"
1568
- || /^\x1b\[(?:83|115)(?::\d*)?(?::\d*)?;3(?::\d+)?u$/.test(data)
1569
- || data === "\x1b[27;3;115~"
1570
- || data === "\x1b[27;3;83~"
1571
- || matchesKey(data, "alt+s");
2059
+ if (action === "Promote to idea") {
2060
+ try {
2061
+ captureIdeaFromText(ctx, selected);
2062
+ } catch (error) {
2063
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2064
+ }
2065
+ }
1572
2066
  }
1573
2067
 
1574
- function getChatJumpShortcutAction(data: string): ChatJumpShortcutAction | null {
1575
- return CHAT_JUMP_SHORTCUTS.find(({ shortcutKey }) => matchesConfiguredShortcut(data, resolvedShortcuts[shortcutKey]))?.action ?? null;
2068
+ function isStashShortcutInput(data: string): boolean {
2069
+ return matchesStashShortcutInput(data, { includePrintableSharpS: config.stashSharpSShortcut });
1576
2070
  }
1577
2071
 
1578
2072
  function isPromptHistoryShortcutInput(data: string): boolean {
@@ -1596,6 +2090,12 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1596
2090
  if (matchesConfiguredShortcut(data, resolvedShortcuts.cutEditor)) {
1597
2091
  return { kind: "cutEditor" };
1598
2092
  }
2093
+ if (matchesConfiguredShortcut(data, resolvedShortcuts.ideaCapture)) {
2094
+ return { kind: "ideaCapture" };
2095
+ }
2096
+ if (matchesConfiguredShortcut(data, resolvedShortcuts.queueOpen)) {
2097
+ return { kind: "queueOpen" };
2098
+ }
1599
2099
  if (matchesConfiguredShortcut(data, bashModeSettings.toggleShortcut)) {
1600
2100
  return { kind: "bashMode" };
1601
2101
  }
@@ -1604,6 +2104,10 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1604
2104
  return chatJumpAction ? { kind: "chat", action: chatJumpAction } : null;
1605
2105
  }
1606
2106
 
2107
+ function getChatJumpShortcutAction(data: string): ChatJumpShortcutAction | null {
2108
+ return CHAT_JUMP_SHORTCUTS.find(({ shortcutKey }) => matchesConfiguredShortcut(data, resolvedShortcuts[shortcutKey]))?.action ?? null;
2109
+ }
2110
+
1607
2111
  function runPowerlineShortcut(ctx: any, action: PowerlineShortcutAction): void {
1608
2112
  if (action.kind === "stashHistory") {
1609
2113
  void openStashHistory(ctx);
@@ -1622,6 +2126,22 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1622
2126
  return;
1623
2127
  }
1624
2128
 
2129
+ if (action.kind === "ideaCapture") {
2130
+ const text = getEditorTextForClipboard(ctx);
2131
+ if (!text) return;
2132
+
2133
+ const item = captureCurrentProjectIdea(ctx, text);
2134
+ if (item) {
2135
+ ctx.ui.setEditorText("");
2136
+ }
2137
+ return;
2138
+ }
2139
+
2140
+ if (action.kind === "queueOpen") {
2141
+ void openQueuePicker(ctx, "queue");
2142
+ return;
2143
+ }
2144
+
1625
2145
  if (action.kind === "bashMode") {
1626
2146
  void setBashModeActive(!bashModeActive, ctx);
1627
2147
  return;
@@ -1684,27 +2204,241 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1684
2204
  : await selectStashedPromptFromHistory(ctx);
1685
2205
  if (!selected) return;
1686
2206
 
2207
+ if (source === "stash") {
2208
+ await handleSelectedStashHistoryEntry(ctx, selected);
2209
+ return;
2210
+ }
2211
+
1687
2212
  await insertSelectedPromptHistoryEntry(ctx, selected);
1688
2213
  }
1689
2214
 
1690
2215
  pi.on("agent_end", async (_event, ctx) => {
1691
2216
  isStreaming = false;
1692
2217
  liveAssistantUsage = null;
2218
+ coreContextUsageCache.reset();
2219
+
2220
+ let hasUI = false;
2221
+ try {
2222
+ hasUI = Boolean(ctx.hasUI);
2223
+ } catch (error) {
2224
+ if (!isStaleExtensionContextError(error)) throw error;
2225
+ currentCtx = null;
2226
+ return;
2227
+ }
2228
+
1693
2229
  currentCtx = ctx;
1694
- if (ctx.hasUI) {
1695
- onVibeAgentEnd(ctx.ui.setWorkingMessage); // working-vibes internal state + reset message
1696
- if (stashedEditorText !== null) {
1697
- if (ctx.ui.getEditorText().trim() === "") {
1698
- ctx.ui.setEditorText(stashedEditorText);
1699
- stashedEditorText = null;
1700
- ctx.ui.setStatus("stash", undefined);
1701
- ctx.ui.notify("Stash restored", "info");
1702
- } else {
1703
- ctx.ui.notify("Stash preserved — clear editor then Alt+S to restore", "info");
2230
+ try {
2231
+ if (hasUI) {
2232
+ onVibeAgentEnd(ctx.ui.setWorkingMessage); // working-vibes internal state + reset message
2233
+ if (stashedEditorText !== null) {
2234
+ if (ctx.ui.getEditorText().trim() === "") {
2235
+ ctx.ui.setEditorText(stashedEditorText);
2236
+ stashedEditorText = null;
2237
+ ctx.ui.setStatus("stash", undefined);
2238
+ ctx.ui.notify("Stash restored", "info");
2239
+ } else {
2240
+ ctx.ui.notify("Stash preserved — clear editor then Alt+S to restore", "info");
2241
+ }
1704
2242
  }
1705
2243
  }
2244
+ } catch (error) {
2245
+ if (!isStaleExtensionContextError(error)) throw error;
2246
+ currentCtx = null;
2247
+ return;
1706
2248
  }
2249
+
1707
2250
  requestStatusRender();
2251
+ if (!powerlineCompacting && !deliverAfterRetrySettles) {
2252
+ schedulePostCompactionDelivery(ctx);
2253
+ }
2254
+ });
2255
+
2256
+ registerCdCommand(pi, () => currentCtx?.cwd ?? process.cwd());
2257
+
2258
+ pi.registerCommand("idea", {
2259
+ description: "Capture an idea without interrupting the current agent. Usage: /idea [@alias|@global|@current] <text> | /idea issue [id]",
2260
+ handler: async (args, ctx) => {
2261
+ currentCtx = ctx;
2262
+ const trimmedArgs = args.trim();
2263
+ const [action, id] = trimmedArgs.split(/\s+/).filter(Boolean);
2264
+ if (action === "issue") {
2265
+ sendIdeaIssueHandoffById(ctx, id);
2266
+ return;
2267
+ }
2268
+
2269
+ const raw = trimmedArgs || getCurrentEditorText(ctx, currentEditor).trim();
2270
+ if (!raw) {
2271
+ ctx.ui.notify("Usage: /idea [@alias|@global|@current] <text> | /idea issue [id]", "info");
2272
+ return;
2273
+ }
2274
+
2275
+ try {
2276
+ const item = captureIdeaFromText(ctx, raw);
2277
+ if (item && !trimmedArgs) {
2278
+ ctx.ui.setEditorText("");
2279
+ }
2280
+ } catch (error) {
2281
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2282
+ }
2283
+ },
2284
+ });
2285
+
2286
+ pi.registerCommand("ideas", {
2287
+ description: "Review or send captured ideas. Usage: /ideas next | /ideas issue [id] | /ideas [send|retry|clear|edit] <id>",
2288
+ handler: async (args, ctx) => {
2289
+ currentCtx = ctx;
2290
+ const parts = args.trim().split(/\s+/).filter(Boolean);
2291
+ const action = parts[0];
2292
+ const id = parts[1];
2293
+
2294
+ if (!action) {
2295
+ await openQueuePicker(ctx, "ideas");
2296
+ return;
2297
+ }
2298
+
2299
+ if (action === "next") {
2300
+ const item = findNextIdea(ctx);
2301
+ if (!item) {
2302
+ ctx.ui.notify("No ideas captured", "info");
2303
+ return;
2304
+ }
2305
+ const updated = queueStore.update(item.id, { status: "queued", target: { kind: "current-session" }, error: undefined });
2306
+ if (updated) deliverQueueItem(ctx, updated);
2307
+ return;
2308
+ }
2309
+
2310
+ if (action === "issue") {
2311
+ sendIdeaIssueHandoffById(ctx, id);
2312
+ return;
2313
+ }
2314
+
2315
+ if (!id) {
2316
+ ctx.ui.notify("Usage: /ideas next | /ideas issue [id] | /ideas [send|retry|clear|edit] <id>", "info");
2317
+ return;
2318
+ }
2319
+
2320
+ const item = queueStore.get(id);
2321
+ if (!item || item.intent !== "idea") {
2322
+ ctx.ui.notify(`No unique idea matches ${id}`, "warning");
2323
+ return;
2324
+ }
2325
+
2326
+ if (action === "send" || action === "retry") {
2327
+ const updated = queueStore.update(item.id, { status: "queued", target: { kind: "current-session" }, error: undefined });
2328
+ if (updated) deliverQueueItem(ctx, updated);
2329
+ return;
2330
+ }
2331
+
2332
+ if (action === "edit") {
2333
+ ctx.ui.setEditorText(item.text);
2334
+ queueStore.clear(item.id);
2335
+ requestQueueRender();
2336
+ return;
2337
+ }
2338
+
2339
+ if (action === "clear") {
2340
+ queueStore.clear(item.id);
2341
+ ctx.ui.notify(`Cleared idea ${item.id}`, "info");
2342
+ requestQueueRender();
2343
+ return;
2344
+ }
2345
+
2346
+ ctx.ui.notify("Usage: /ideas next | /ideas issue [id] | /ideas [send|retry|clear|edit] <id>", "info");
2347
+ },
2348
+ });
2349
+
2350
+ pi.registerCommand("queue", {
2351
+ description: "Manage Powerline queued prompts and project aliases",
2352
+ handler: async (args, ctx) => {
2353
+ currentCtx = ctx;
2354
+ const parts = args.trim().split(/\s+/).filter(Boolean);
2355
+ const action = parts[0];
2356
+
2357
+ if (!action) {
2358
+ await openQueuePicker(ctx, "queue");
2359
+ return;
2360
+ }
2361
+
2362
+ if (action === "alias") {
2363
+ const alias = parts[1];
2364
+ const aliasPath = parts.slice(2).join(" ") || ctx.cwd || process.cwd();
2365
+ if (!alias) {
2366
+ ctx.ui.notify("Usage: /queue alias <name> [path]", "info");
2367
+ return;
2368
+ }
2369
+ try {
2370
+ queueStore.setAlias(alias, aliasPath);
2371
+ ctx.ui.notify(`Alias @${alias} → ${aliasPath}`, "info");
2372
+ } catch (error) {
2373
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2374
+ }
2375
+ return;
2376
+ }
2377
+
2378
+ if (action === "send" || action === "retry") {
2379
+ const id = parts[1];
2380
+ if (!id) {
2381
+ const item = queueStore.queuedDeliveryItems(getQueueContext(ctx))[0];
2382
+ if (!item) {
2383
+ ctx.ui.notify("No queued item to send", "info");
2384
+ return;
2385
+ }
2386
+ sendOrRetryQueueItem(ctx, item.id);
2387
+ return;
2388
+ }
2389
+ sendOrRetryQueueItem(ctx, id);
2390
+ return;
2391
+ }
2392
+
2393
+ if (action === "clear") {
2394
+ const id = parts[1];
2395
+ if (id === "all") {
2396
+ const active = queueStore.activeItems(getQueueContext(ctx)).filter((item) => item.intent !== "idea");
2397
+ for (const item of active) queueStore.clear(item.id);
2398
+ ctx.ui.notify(`Cleared ${active.length} queued item${active.length === 1 ? "" : "s"}`, "info");
2399
+ requestQueueRender();
2400
+ return;
2401
+ }
2402
+ if (!id) {
2403
+ ctx.ui.notify("Usage: /queue clear <id|all>", "info");
2404
+ return;
2405
+ }
2406
+ const item = queueStore.get(id);
2407
+ if (!item || item.intent === "idea") {
2408
+ ctx.ui.notify(`No unique queued item matches ${id}`, "warning");
2409
+ return;
2410
+ }
2411
+ queueStore.clear(item.id);
2412
+ ctx.ui.notify(`Cleared ${item.id}`, "info");
2413
+ requestQueueRender();
2414
+ return;
2415
+ }
2416
+
2417
+ if (action === "target") {
2418
+ const id = parts[1];
2419
+ const spec = parts[2];
2420
+ if (!id || !spec) {
2421
+ ctx.ui.notify("Usage: /queue target <id> @alias|global|current", "info");
2422
+ return;
2423
+ }
2424
+ const item = queueStore.get(id);
2425
+ if (!item) {
2426
+ ctx.ui.notify(`No unique queue item matches ${id}`, "warning");
2427
+ return;
2428
+ }
2429
+ try {
2430
+ const target = resolveCommandTarget(ctx, spec);
2431
+ queueStore.update(item.id, { target });
2432
+ ctx.ui.notify(`Retargeted ${item.id}`, "info");
2433
+ requestQueueRender();
2434
+ } catch (error) {
2435
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2436
+ }
2437
+ return;
2438
+ }
2439
+
2440
+ ctx.ui.notify("Usage: /queue [send|retry|clear|target|alias]", "info");
2441
+ },
1708
2442
  });
1709
2443
 
1710
2444
  pi.registerCommand("guide", {
@@ -1778,7 +2512,6 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1778
2512
  ctx.ui.setStatus("stash", undefined);
1779
2513
  restoreFooterStatusRepaintHook?.();
1780
2514
  restoreFooterStatusRepaintHook = null;
1781
- teardownFixedEditorCompositor();
1782
2515
  stashShortcutInputUnsubscribe?.();
1783
2516
  stashShortcutInputUnsubscribe = null;
1784
2517
  // Clear all custom UI components
@@ -1789,6 +2522,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1789
2522
  ctx.ui.setWidget("powerline-secondary", undefined);
1790
2523
  ctx.ui.setWidget("powerline-bash-transcript", undefined);
1791
2524
  ctx.ui.setWidget("powerline-status", undefined);
2525
+ ctx.ui.setWidget("powerline-queue-preview", undefined);
1792
2526
  ctx.ui.setWidget("powerline-last-prompt", undefined);
1793
2527
  footerDataRef = null;
1794
2528
  tuiRef = null;
@@ -1801,34 +2535,19 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1801
2535
  }
1802
2536
 
1803
2537
  const normalizedArgs = args.trim().toLowerCase();
1804
- const mouseScrollMatch = /^mouse-scroll(?:\s+(on|off|toggle))?$/.exec(normalizedArgs);
1805
- if (mouseScrollMatch) {
1806
- const mode = mouseScrollMatch[1] ?? "toggle";
1807
- config.mouseScroll = mode === "toggle" ? !config.mouseScroll : mode === "on";
1808
- if (enabled && ctx.hasUI && config.fixedEditor && tuiRef && currentEditor) {
1809
- installFixedEditorCompositor(ctx, tuiRef);
1810
- }
1811
-
1812
- if (writePowerlineOptionSetting(ctx.cwd, { mouseScroll: config.mouseScroll }, config.preset)) {
1813
- ctx.ui.notify(`Powerline mouse scroll ${config.mouseScroll ? "enabled" : "disabled"}`, "info");
2538
+ const placementMatch = /^placement(?:\s+(above|below|toggle))?$/.exec(normalizedArgs);
2539
+ if (placementMatch) {
2540
+ const requestedPlacement = placementMatch[1];
2541
+ config.placement = requestedPlacement === "above" || requestedPlacement === "below"
2542
+ ? requestedPlacement
2543
+ : config.placement === "above" ? "below" : "above";
2544
+ config.invalidPlacement = null;
2545
+ if (enabled && ctx.hasUI) setupCustomEditor(ctx);
2546
+
2547
+ if (writePowerlineOptionSetting(ctx.cwd, { placement: config.placement }, config.preset)) {
2548
+ ctx.ui.notify(`Powerline placement set to: ${config.placement}`, "info");
1814
2549
  } else {
1815
- ctx.ui.notify(`Powerline mouse scroll ${config.mouseScroll ? "enabled" : "disabled"} (not persisted; check settings.json)`, "warning");
1816
- }
1817
- return;
1818
- }
1819
-
1820
- const fixedEditorMatch = /^fixed-editor(?:\s+(on|off|toggle))?$/.exec(normalizedArgs);
1821
- if (fixedEditorMatch) {
1822
- const mode = fixedEditorMatch[1] ?? "toggle";
1823
- config.fixedEditor = mode === "toggle" ? !config.fixedEditor : mode === "on";
1824
- if (enabled && ctx.hasUI) {
1825
- setupCustomEditor(ctx);
1826
- }
1827
-
1828
- if (writePowerlineOptionSetting(ctx.cwd, { fixedEditor: config.fixedEditor }, config.preset)) {
1829
- ctx.ui.notify(`Powerline fixed editor ${config.fixedEditor ? "enabled" : "disabled"}`, "info");
1830
- } else {
1831
- ctx.ui.notify(`Powerline fixed editor ${config.fixedEditor ? "enabled" : "disabled"} (not persisted; check settings.json)`, "warning");
2550
+ ctx.ui.notify(`Powerline placement set to: ${config.placement} (not persisted; check settings.json)`, "warning");
1832
2551
  }
1833
2552
  return;
1834
2553
  }
@@ -1910,65 +2629,6 @@ export default function powerlineFooter(pi: ExtensionAPI) {
1910
2629
  },
1911
2630
  });
1912
2631
 
1913
- pi.registerShortcut(bashModeSettings.toggleShortcut, {
1914
- description: "Toggle bash mode",
1915
- handler: async (ctx) => {
1916
- if (!enabled || !ctx.hasUI) return;
1917
- await setBashModeActive(!bashModeActive, ctx);
1918
- },
1919
- });
1920
-
1921
- pi.registerShortcut("alt+s", {
1922
- description: "Stash/restore editor text",
1923
- handler: async (ctx) => {
1924
- if (!enabled || !ctx.hasUI) return;
1925
- stashOrRestoreEditorText(ctx);
1926
- },
1927
- });
1928
-
1929
- pi.registerShortcut(resolvedShortcuts.stashHistory, {
1930
- description: "Open prompt history picker",
1931
- handler: async (ctx) => {
1932
- if (!enabled || !ctx.hasUI) return;
1933
- await openStashHistory(ctx);
1934
- },
1935
- });
1936
-
1937
- pi.registerShortcut(resolvedShortcuts.copyEditor, {
1938
- description: "Copy full editor text",
1939
- handler: async (ctx) => {
1940
- if (!enabled || !ctx.hasUI) return;
1941
-
1942
- const text = getEditorTextForClipboard(ctx);
1943
- if (!text) return;
1944
-
1945
- copyTextToClipboard(ctx, text, "Copied editor text");
1946
- },
1947
- });
1948
-
1949
- pi.registerShortcut(resolvedShortcuts.cutEditor, {
1950
- description: "Cut full editor text",
1951
- handler: async (ctx) => {
1952
- if (!enabled || !ctx.hasUI) return;
1953
-
1954
- const text = getEditorTextForClipboard(ctx);
1955
- if (!text) return;
1956
-
1957
- copyTextToClipboard(ctx, text);
1958
- ctx.ui.setEditorText("");
1959
- ctx.ui.notify("Cut editor text", "info");
1960
- },
1961
- });
1962
-
1963
- for (const { shortcutKey, description, action } of CHAT_JUMP_SHORTCUTS) {
1964
- pi.registerShortcut(resolvedShortcuts[shortcutKey], {
1965
- description,
1966
- handler: async (ctx) => {
1967
- if (!enabled || !ctx.hasUI) return;
1968
- runPowerlineShortcut(ctx, { kind: "chat", action });
1969
- },
1970
- });
1971
- }
1972
2632
 
1973
2633
  // Command to set working message theme
1974
2634
  pi.registerCommand("vibe", {
@@ -2040,17 +2700,13 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2040
2700
 
2041
2701
  // /vibe generate <theme> [count] - generate vibes and save to file
2042
2702
  if (subcommand === "generate") {
2043
- const theme = parts[1];
2044
- const parsedCount = Number.parseInt(parts[2] ?? "", 10);
2045
- const count = Number.isFinite(parsedCount)
2046
- ? Math.min(Math.max(Math.floor(parsedCount), 1), 500)
2047
- : 100;
2048
-
2049
- if (!theme) {
2703
+ const parsed = parseVibeGenerateArgs(parts.slice(1));
2704
+ if (!parsed) {
2050
2705
  ctx.ui.notify("Usage: /vibe generate <theme> [count]", "error");
2051
2706
  return;
2052
2707
  }
2053
2708
 
2709
+ const { theme, count } = parsed;
2054
2710
  ctx.ui.notify(`Generating ${count} vibes for "${theme}"...`, "info");
2055
2711
 
2056
2712
  const result = await generateVibesBatch(theme, count);
@@ -2108,53 +2764,14 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2108
2764
  const presetDef = getPreset(config.preset);
2109
2765
  const colors: ColorScheme = presetDef.colors ?? getDefaultColors();
2110
2766
 
2111
- // Build usage stats and get thinking level from session
2112
- let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
2113
- let lastAssistant: AssistantMessage | undefined;
2114
- let thinkingLevelFromSession: string | null = null;
2115
-
2116
- const sessionEvents = ctx.sessionManager?.getBranch?.() ?? [];
2117
- for (const e of sessionEvents) {
2118
- if (!isRecord(e)) {
2119
- continue;
2120
- }
2121
-
2122
- // Check for thinking level change entries
2123
- if (e.type === "thinking_level_change" && typeof e.thinkingLevel === "string") {
2124
- thinkingLevelFromSession = e.thinkingLevel;
2125
- }
2126
-
2127
- if (e.type !== "message") {
2128
- continue;
2129
- }
2130
-
2131
- const subagentUsage = readSubagentToolResultUsage(e.message);
2132
- if (subagentUsage) {
2133
- input += subagentUsage.input;
2134
- output += subagentUsage.output;
2135
- cacheRead += subagentUsage.cacheRead;
2136
- cacheWrite += subagentUsage.cacheWrite;
2137
- cost += subagentUsage.cost;
2138
- continue;
2139
- }
2140
-
2141
- if (!isSessionAssistantMessage(e.message)) {
2142
- continue;
2143
- }
2144
-
2145
- const m = e.message;
2146
- if (m.stopReason === "error" || m.stopReason === "aborted") {
2147
- continue;
2148
- }
2149
- input += m.usage.input;
2150
- output += m.usage.output;
2151
- cacheRead += m.usage.cacheRead;
2152
- cacheWrite += m.usage.cacheWrite;
2153
- cost += m.usage.cost.total;
2154
- if (getUsageTokenTotal(m.usage) > 0) {
2155
- lastAssistant = m;
2156
- }
2157
- }
2767
+ // Build usage stats and get thinking level from session (cached; the full
2768
+ // event list is only re-scanned when events are appended or the trailing
2769
+ // event's stats-relevant fields change, e.g. in-place streaming updates)
2770
+ const sessionEvents = sessionBranchCache.get(ctx.sessionManager);
2771
+ const tokenStats = tokenStatsCache.get(sessionEvents);
2772
+ let { input, output, cacheRead, cacheWrite, cost, subagentCost } = tokenStats;
2773
+ const lastAssistant = tokenStats.lastAssistant;
2774
+ const thinkingLevelFromSession = tokenStats.thinkingLevelFromSession;
2158
2775
 
2159
2776
  for (const subagentUsage of completedAsyncSubagentUsage.values()) {
2160
2777
  input += subagentUsage.input;
@@ -2166,14 +2783,25 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2166
2783
 
2167
2784
  // Calculate context percentage.
2168
2785
  const latestUsage = isStreaming ? liveAssistantUsage ?? lastAssistant?.usage : lastAssistant?.usage;
2169
- const coreContextUsage = isStreaming && liveAssistantUsage ? null : readCoreContextUsage(ctx);
2170
- const contextTokens = coreContextUsage?.contextTokens ?? (latestUsage ? getUsageTokenTotal(latestUsage) : 0);
2171
- const contextWindow = coreContextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
2172
- const contextPercent = coreContextUsage?.contextPercent ?? (contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0);
2786
+ const coreContextUsage = isStreaming && liveAssistantUsage ? null : coreContextUsageCache.get(ctx);
2787
+ const fallbackContextTokens = latestUsage ? getUsageTokenTotal(latestUsage) : 0;
2788
+ const {
2789
+ contextTokens,
2790
+ contextWindow,
2791
+ contextPercent,
2792
+ } = resolveDisplayContextUsage({
2793
+ coreContextUsage,
2794
+ unknownCoreFallback: approximateContextUsage,
2795
+ fallbackContextTokens,
2796
+ fallbackContextWindow: ctx.model?.contextWindow ?? 0,
2797
+ });
2798
+ const contextApproximate = coreContextUsage?.contextTokens === null && approximateContextUsage !== null;
2799
+
2800
+ const segmentOptions = mergeSegmentOptions(presetDef.segmentOptions, config.segmentOptions);
2173
2801
 
2174
2802
  // Get git status (cached)
2175
2803
  const gitBranch = footerDataRef?.getGitBranch() ?? null;
2176
- const gitStatus = getGitStatus(gitBranch);
2804
+ const gitStatus = getGitStatus(gitBranch, segmentOptions.git?.polling);
2177
2805
  const extensionStatuses = footerDataRef?.getExtensionStatuses() ?? new Map();
2178
2806
  const customItemsById = new Map(config.customItems.map((item) => [item.id, item]));
2179
2807
  const hiddenExtensionStatusKeys = collectHiddenExtensionStatusKeys(config.customItems);
@@ -2184,6 +2812,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2184
2812
  : false;
2185
2813
 
2186
2814
  const thinkingLevel = currentThinkingLevel ?? thinkingLevelFromSession ?? getThinkingLevelFn?.() ?? "off";
2815
+ const queueSummary = queueStore.summarize(getQueueContext(ctx), powerlineCompacting);
2187
2816
 
2188
2817
  return {
2189
2818
  model: ctx.model,
@@ -2191,12 +2820,15 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2191
2820
  sessionId: ctx.sessionManager?.getSessionId?.(),
2192
2821
  sessionName: getCurrentSessionName(ctx),
2193
2822
  cwd: ctx.cwd,
2194
- usageStats: { input, output, cacheRead, cacheWrite, cost },
2823
+ usageStats: { input, output, cacheRead, cacheWrite, cost, subagentCost },
2824
+ contextTokens,
2195
2825
  contextPercent,
2196
2826
  contextWindow,
2827
+ contextApproximate,
2197
2828
  autoCompactEnabled: ctx.settingsManager?.getCompactionSettings?.()?.enabled ?? true,
2198
2829
  customCompactionEnabled: customCompactionEnabled || extensionStatuses.has(CUSTOM_COMPACTION_STATUS_KEY),
2199
2830
  usingSubscription,
2831
+ queueSummary,
2200
2832
  sessionStartTime,
2201
2833
  shellModeActive: bashModeActive,
2202
2834
  shellRunning: shellSession?.state.running ?? false,
@@ -2206,7 +2838,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2206
2838
  extensionStatuses,
2207
2839
  hiddenExtensionStatusKeys,
2208
2840
  customItemsById,
2209
- options: presetDef.segmentOptions ?? {},
2841
+ options: segmentOptions,
2210
2842
  theme,
2211
2843
  colors,
2212
2844
  };
@@ -2235,7 +2867,19 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2235
2867
  }
2236
2868
 
2237
2869
  const presetDef = getPreset(config.preset);
2238
- const segmentCtx = buildSegmentContext(currentCtx, theme);
2870
+ let segmentCtx: SegmentContext;
2871
+ try {
2872
+ segmentCtx = buildSegmentContext(currentCtx, theme);
2873
+ } catch (error) {
2874
+ if (!isStaleExtensionContextError(error)) throw error;
2875
+ currentCtx = null;
2876
+ lastLayoutWidth = width;
2877
+ lastLayoutResult = { topContent: "", secondaryContent: "" };
2878
+ lastLayoutTimestamp = now;
2879
+ layoutDirty = false;
2880
+ forceNextLayoutRecompute = false;
2881
+ return lastLayoutResult;
2882
+ }
2239
2883
 
2240
2884
  lastLayoutWidth = width;
2241
2885
  lastLayoutResult = computeResponsiveLayout(segmentCtx, presetDef, width);
@@ -2265,7 +2909,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2265
2909
  return notifications;
2266
2910
  }
2267
2911
 
2268
- function renderPowerlineTopLines(width: number, theme: Theme): string[] {
2912
+ function renderPowerlinePrimaryLines(width: number, theme: Theme): string[] {
2269
2913
  if (!currentCtx) return [];
2270
2914
 
2271
2915
  const layout = getResponsiveLayout(width, theme);
@@ -2279,6 +2923,23 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2279
2923
  return layout.secondaryContent ? [layout.secondaryContent] : [];
2280
2924
  }
2281
2925
 
2926
+ function renderPowerlineQueuePreviewLines(width: number, theme: Theme): string[] {
2927
+ if (!currentCtx) return [];
2928
+ const summary = queueStore.summarize(getQueueContext(currentCtx), powerlineCompacting);
2929
+ if (!summary.leadingText) return [];
2930
+
2931
+ const prefix = summary.leadingStatus === "blocked" || summary.leadingStatus === "failed"
2932
+ ? "blocked: "
2933
+ : summary.leadingStatus === "delivering"
2934
+ ? "sending: "
2935
+ : summary.leadingIntent === "idea"
2936
+ ? "idea: "
2937
+ : "queued: ";
2938
+ const text = `${prefix}${summary.leadingText.replace(/\s+/g, " ").trim()}`;
2939
+ const color = summary.leadingStatus === "blocked" || summary.leadingStatus === "failed" ? "warning" : "dim";
2940
+ return [` ${theme.fg(color, truncateToWidth(text, Math.max(1, width - 1), "…"))}`];
2941
+ }
2942
+
2282
2943
  function renderBashTranscriptLines(width: number, theme: Theme): string[] {
2283
2944
  if (!bashModeActive) return [];
2284
2945
 
@@ -2383,8 +3044,8 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2383
3044
  terminal: tui.terminal,
2384
3045
  mouseScroll: config.mouseScroll,
2385
3046
  keyboardScrollShortcuts: {
2386
- up: resolvedShortcuts.scrollChatUp,
2387
- down: resolvedShortcuts.scrollChatDown,
3047
+ up: resolvedShortcuts.scrollChatUp ?? "super+up",
3048
+ down: resolvedShortcuts.scrollChatDown ?? "super+down",
2388
3049
  },
2389
3050
  shouldCaptureKeyboardScroll: () => Reflect.get(tui, "focusedComponent") === currentEditor,
2390
3051
  onCopySelection: (text) => copyTextToClipboard(ctx, text),
@@ -2400,7 +3061,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2400
3061
  width,
2401
3062
  terminalRows,
2402
3063
  statusLines: [...aboveWidgetLines, ...renderPowerlineStatusLines(width), ...statusContainerLines],
2403
- topLines: renderPowerlineTopLines(width, theme),
3064
+ topLines: renderPowerlinePrimaryLines(width, theme),
2404
3065
  editorLines: fixedEditorContainer ? compositor.renderHidden(fixedEditorContainer, width) : [],
2405
3066
  secondaryLines: [...renderPowerlineSecondaryLines(width, theme), ...belowWidgetLines],
2406
3067
  transcriptLines: renderBashTranscriptLines(width, theme),
@@ -2530,9 +3191,9 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2530
3191
  resetLayoutCache();
2531
3192
  },
2532
3193
  render(width: number): string[] {
2533
- return renderPowerlineTopLines(width, theme);
3194
+ return renderPowerlinePrimaryLines(width, theme);
2534
3195
  },
2535
- }), { placement: "aboveEditor" });
3196
+ }), { placement: config.placement === "below" ? "belowEditor" : "aboveEditor" });
2536
3197
 
2537
3198
  ctx.ui.setWidget("powerline-secondary", (_tui: any, theme: Theme) => ({
2538
3199
  dispose() {},
@@ -2552,6 +3213,14 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2552
3213
  },
2553
3214
  }), { placement: "belowEditor" });
2554
3215
 
3216
+ ctx.ui.setWidget("powerline-queue-preview", (_tui: any, theme: Theme) => ({
3217
+ dispose() {},
3218
+ invalidate() {},
3219
+ render(width: number): string[] {
3220
+ return renderPowerlineQueuePreviewLines(width, theme);
3221
+ },
3222
+ }), { placement: "belowEditor" });
3223
+
2555
3224
  ctx.ui.setWidget("powerline-last-prompt", () => ({
2556
3225
  dispose() {},
2557
3226
  invalidate() {},
@@ -2590,16 +3259,18 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2590
3259
  })
2591
3260
  : null;
2592
3261
 
2593
- teardownFixedEditorCompositor();
2594
3262
  ctx.ui.setWidget("powerline-top", undefined);
2595
3263
  ctx.ui.setWidget("powerline-secondary", undefined);
2596
3264
  ctx.ui.setWidget("powerline-bash-transcript", undefined);
2597
3265
  ctx.ui.setWidget("powerline-status", undefined);
3266
+ ctx.ui.setWidget("powerline-queue-preview", undefined);
2598
3267
  ctx.ui.setWidget("powerline-last-prompt", undefined);
2599
3268
 
2600
3269
  let autocompleteFixed = false;
3270
+ const previousEditorFactory = typeof ctx.ui.getEditorComponent === "function" ? ctx.ui.getEditorComponent() : undefined;
2601
3271
 
2602
3272
  const editorFactory = (tui: any, editorTheme: any, keybindings: any) => {
3273
+ const previousEditor = previousEditorFactory?.(tui, editorTheme, keybindings);
2603
3274
  const editor = new BashModeEditor(tui, editorTheme, keybindings, {
2604
3275
  keybindings,
2605
3276
  isBashModeActive: () => bashModeActive,
@@ -2608,7 +3279,6 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2608
3279
  void setBashModeActive(false, ctx);
2609
3280
  },
2610
3281
  onSubmitCommand: (command) => void runShellCommand(command, ctx),
2611
- onEditorSubmit: () => followSubmittedEditorToBottom(),
2612
3282
  editorBoundaryShortcuts: {
2613
3283
  start: resolvedShortcuts.editorStart,
2614
3284
  end: resolvedShortcuts.editorEnd,
@@ -2635,18 +3305,20 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2635
3305
  },
2636
3306
  });
2637
3307
 
2638
- const getInstalledAutocompleteProvider = (): AutocompleteProvider | undefined => {
2639
- const candidate = Reflect.get(editor, "autocompleteProvider");
2640
- if (!candidate || typeof candidate !== "object") {
2641
- return undefined;
2642
- }
2643
- if (typeof Reflect.get(candidate, "getSuggestions") !== "function") {
2644
- return undefined;
2645
- }
2646
- if (typeof Reflect.get(candidate, "applyCompletion") !== "function") {
2647
- return undefined;
3308
+ let installingPowerlineAutocompleteProvider = false;
3309
+ const originalSetAutocompleteProvider = editor.setAutocompleteProvider.bind(editor);
3310
+ editor.setAutocompleteProvider = (provider: AutocompleteProvider) => {
3311
+ if (installingPowerlineAutocompleteProvider) {
3312
+ originalSetAutocompleteProvider(provider);
3313
+ return;
2648
3314
  }
2649
- return candidate;
3315
+
3316
+ originalSetAutocompleteProvider(passAutocompleteProviderThroughPreviousEditor(provider, previousEditor));
3317
+ attachAutocompleteProvider();
3318
+ };
3319
+
3320
+ const getInstalledAutocompleteProvider = (): AutocompleteProvider | undefined => {
3321
+ return getEditorAutocompleteProvider(editor) ?? getEditorAutocompleteProvider(previousEditor);
2650
3322
  };
2651
3323
 
2652
3324
  const attachAutocompleteProvider = (): boolean => {
@@ -2656,26 +3328,17 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2656
3328
 
2657
3329
  const bashProvider = new BashAutocompleteProvider();
2658
3330
  const oneOffBashProvider = new OneOffBashAutocompleteProvider();
2659
- editor.installAutocompleteProvider(
2660
- new ModeAwareAutocompleteProvider(defaultProvider, bashProvider, oneOffBashProvider, () => bashModeActive),
2661
- );
3331
+ installingPowerlineAutocompleteProvider = true;
3332
+ try {
3333
+ editor.installAutocompleteProvider(
3334
+ new ModeAwareAutocompleteProvider(defaultProvider, bashProvider, oneOffBashProvider, () => bashModeActive),
3335
+ );
3336
+ } finally {
3337
+ installingPowerlineAutocompleteProvider = false;
3338
+ }
2662
3339
  return true;
2663
3340
  };
2664
3341
 
2665
- let inheritedOnSubmit: unknown;
2666
- Object.defineProperty(editor, "onSubmit", {
2667
- configurable: true,
2668
- get: () => inheritedOnSubmit,
2669
- set(handler: unknown) {
2670
- inheritedOnSubmit = typeof handler === "function"
2671
- ? (text: string) => {
2672
- followSubmittedEditorToBottom();
2673
- handler(text);
2674
- }
2675
- : handler;
2676
- },
2677
- });
2678
-
2679
3342
  currentEditor = editor;
2680
3343
  trackPromptHistory(editor);
2681
3344
  restorePromptHistory(editor);
@@ -2685,6 +3348,62 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2685
3348
  editor.handleInput = (data: string) => {
2686
3349
  lastEditorInputAt = Date.now();
2687
3350
 
3351
+ const isSubmit = keybindings.matches(data, "tui.input.submit") && !keybindings.matches(data, "tui.input.newLine");
3352
+ const isFollowUpSubmit = keybindings.matches(data, "app.message.followUp");
3353
+ if (!bashModeActive && (isSubmit || isFollowUpSubmit)) {
3354
+ const sigilCapture = parseSigilIdeaCapture(editor.getExpandedText(), config.queue.captureSigil);
3355
+ if (sigilCapture) {
3356
+ try {
3357
+ const item = captureIdeaFromParsedInput(ctx, sigilCapture);
3358
+ if (item) {
3359
+ editor.addToHistory?.(editor.getExpandedText().trim());
3360
+ editor.setText("");
3361
+ }
3362
+ } catch (error) {
3363
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
3364
+ }
3365
+ scheduleDismissWelcome(ctx);
3366
+ return;
3367
+ }
3368
+ }
3369
+
3370
+ if (!powerlineCompacting && !bashModeActive && isSubmit && typeof ctx.compact === "function") {
3371
+ const editorText = editor.getExpandedText().trim();
3372
+ const compactQueuedPrompt = parseCompactQueuedPrompt(editorText);
3373
+ if (editorText === "/compact" || compactQueuedPrompt) {
3374
+ editor.addToHistory?.(editorText);
3375
+ editor.setText("");
3376
+ if (compactQueuedPrompt) {
3377
+ capturePostCompactPrompt(ctx, compactQueuedPrompt);
3378
+ }
3379
+ powerlineCompacting = true;
3380
+ deliverAfterRetrySettles = false;
3381
+ requestQueueRender();
3382
+ ctx.compact({
3383
+ onError: (error: Error) => {
3384
+ finishFailedCompaction(ctx, error.message);
3385
+ ctx.ui.notify(error.message, "error");
3386
+ },
3387
+ });
3388
+ scheduleDismissWelcome(ctx);
3389
+ return;
3390
+ }
3391
+ }
3392
+
3393
+ if (powerlineCompacting && !bashModeActive && (isSubmit || isFollowUpSubmit)) {
3394
+ const text = editor.getExpandedText().trim();
3395
+ if (!text) return;
3396
+ if (text.startsWith("/")) {
3397
+ originalHandleInput(data);
3398
+ return;
3399
+ }
3400
+ editor.addToHistory?.(text);
3401
+ editor.setText("");
3402
+ capturePostCompactPrompt(ctx, text);
3403
+ scheduleDismissWelcome(ctx);
3404
+ return;
3405
+ }
3406
+
2688
3407
  if (isStashShortcutInput(data)) {
2689
3408
  stashOrRestoreEditorText(ctx);
2690
3409
  return;
@@ -2700,15 +3419,13 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2700
3419
  autocompleteFixed = true;
2701
3420
  snapshotPromptHistory(editor);
2702
3421
  ctx.ui.setEditorComponent(editorFactory);
2703
- if (config.fixedEditor) {
2704
- installFixedEditorCompositor(ctx, tui);
2705
- }
2706
3422
  currentEditor?.handleInput(data);
2707
3423
  return;
2708
3424
  }
2709
3425
 
2710
3426
  attachAutocompleteProvider();
2711
3427
  const followUpText = keybindings.matches(data, "app.message.followUp") ? getCurrentEditorText(ctx, editor) : "";
3428
+ scheduleDismissWelcome(ctx);
2712
3429
  originalHandleInput(data);
2713
3430
  if (hasNonWhitespaceText(followUpText) && !hasNonWhitespaceText(getCurrentEditorText(ctx, editor))) {
2714
3431
  followSubmittedEditorToBottom();
@@ -2721,9 +3438,14 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2721
3438
  return originalRender(width);
2722
3439
  }
2723
3440
 
2724
- const bc = (s: string) => `${getFgAnsiCode("sep")}${s}${ansi.reset}`;
2725
- const promptGlyph = bashModeActive ? "$" : config.fixedEditorPromptGlyph;
2726
- const prompt = promptGlyph ? `${ansi.getFgAnsi(200, 200, 200)}${promptGlyph}${ansi.reset}` : "";
3441
+ const bc = (s: string) =>
3442
+ typeof editor.borderColor === "function"
3443
+ ? editor.borderColor(s)
3444
+ : `${getFgAnsiCode("sep")}${s}${ansi.reset}`;
3445
+ const captureDraft = !bashModeActive && isSigilIdeaDraft(editor.getExpandedText());
3446
+ const promptGlyph = bashModeActive ? "$" : captureDraft ? captureSigilGlyph() : config.fixedEditorPromptGlyph;
3447
+ const promptColor = captureDraft ? getFgAnsiCode("queue") : ansi.getFgAnsi(200, 200, 200);
3448
+ const prompt = promptGlyph ? `${promptColor}${promptGlyph}${ansi.reset}` : "";
2727
3449
  const promptPrefix = prompt ? ` ${prompt} ` : " ";
2728
3450
  const contPrefix = " ";
2729
3451
  const contentWidth = Math.max(1, width - 1);
@@ -2771,10 +3493,12 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2771
3493
  tuiRef = tui;
2772
3494
  installFooterStatusRepaintHook(footerData);
2773
3495
  const unsub = footerData.onBranchChange(() => requestStatusRender());
3496
+ const unsubGitUpdates = subscribeGitUpdates(() => requestStatusRender());
2774
3497
 
2775
3498
  return {
2776
3499
  dispose() {
2777
3500
  unsub();
3501
+ unsubGitUpdates();
2778
3502
  restoreFooterStatusRepaintHook?.();
2779
3503
  restoreFooterStatusRepaintHook = null;
2780
3504
  },
@@ -2782,18 +3506,12 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2782
3506
  requestStatusRender();
2783
3507
  },
2784
3508
  render(): string[] {
2785
- return [];
3509
+ return [""];
2786
3510
  },
2787
3511
  };
2788
3512
  });
2789
3513
 
2790
- if (config.fixedEditor) {
2791
- if (tuiRef) {
2792
- installFixedEditorCompositor(ctx, tuiRef);
2793
- }
2794
- } else {
2795
- installPowerlineWidgets(ctx);
2796
- }
3514
+ installPowerlineWidgets(ctx);
2797
3515
  }
2798
3516
 
2799
3517
  function setupWelcomeHeader(ctx: any, newFeatures: GuideFeature[] = []) {
@@ -2801,8 +3519,9 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2801
3519
  const providerName = ctx.model?.provider || "Unknown";
2802
3520
  const loadedCounts = discoverLoadedCounts();
2803
3521
  const recentSessions = getRecentSessions(3);
3522
+ const initialContextTokens = estimateInitialContextTokens(ctx);
2804
3523
 
2805
- const header = new WelcomeHeader(modelName, providerName, recentSessions, loadedCounts, "compact", newFeatures);
3524
+ const header = new WelcomeHeader(modelName, providerName, recentSessions, loadedCounts, "compact", newFeatures, initialContextTokens);
2806
3525
  welcomeHeaderActive = true;
2807
3526
  markGuideVersionSeen(VERSION, getSettingsPath());
2808
3527
 
@@ -2833,6 +3552,18 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2833
3552
  return;
2834
3553
  }
2835
3554
 
3555
+ const sessionEvents = ctx.sessionManager?.getBranch?.() ?? [];
3556
+ const hasActivity = sessionEvents.some((entry: unknown) => {
3557
+ if (!isRecord(entry)) return false;
3558
+ if (entry.type === "tool_call" || entry.type === "tool_result") return true;
3559
+ return entry.type === "message" && isRecord(entry.message) && entry.message.role === "assistant";
3560
+ });
3561
+ if (hasActivity) {
3562
+ return;
3563
+ }
3564
+
3565
+ const initialContextTokens = estimateInitialContextTokens(ctx);
3566
+
2836
3567
  ctx.ui.custom(
2837
3568
  (_tui: any, _theme: any, _keybindings: any, done: (result: void) => void) => {
2838
3569
  const welcome = new WelcomeComponent(
@@ -2842,6 +3573,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2842
3573
  loadedCounts,
2843
3574
  "full",
2844
3575
  newFeatures,
3576
+ initialContextTokens,
2845
3577
  );
2846
3578
  markGuideVersionSeen(VERSION, getSettingsPath());
2847
3579
  guideOverlayBlocking = true;
@@ -2880,7 +3612,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2880
3612
  horizontalAlign: "center",
2881
3613
  }),
2882
3614
  },
2883
- ).catch((error) => {
3615
+ ).catch((error: unknown) => {
2884
3616
  console.debug("[powerline-footer] Welcome overlay failed:", error);
2885
3617
  });
2886
3618
  }, 100);