@sayknow-cli/coding-agent 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/telegram-cli.d.ts +13 -0
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/commands/telegram.d.ts +22 -0
- package/dist/types/config/keybindings.d.ts +12 -6
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +284 -4
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/config/telegram-autostart.d.ts +14 -0
- package/dist/types/config/telegram-env-bridge.d.ts +18 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/defaults/skc-defaults.d.ts +7 -0
- package/dist/types/discovery/cursor.d.ts +4 -1
- package/dist/types/discovery/index.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/i18n/messages/en.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -1
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +5 -1
- package/dist/types/modes/components/skill-message.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +15 -3
- package/dist/types/modes/components/two-column-body.d.ts +9 -0
- package/dist/types/modes/components/welcome.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +6 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
- package/dist/types/modes/slash-command-visibility.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/types.d.ts +5 -1
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-daemon.d.ts +4 -10
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
- package/dist/types/runtime-mcp/config.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
- package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
- package/dist/types/skc-runtime/state-writer.d.ts +1 -0
- package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +7 -0
- package/dist/types/tools/skill.d.ts +1 -1
- package/dist/types/utils/cmux-workspace.d.ts +52 -0
- package/dist/types/web/search/index.d.ts +4 -2
- package/dist/types/web/search/provider.d.ts +14 -0
- package/dist/types/web/search/providers/utils.d.ts +42 -13
- package/dist/types/web/search/render.d.ts +1 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +3 -0
- package/scripts/verify-insane-vendor.ts +0 -5
- package/src/capability/mcp.ts +2 -0
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +4 -2
- package/src/cli/telegram-cli.ts +156 -0
- package/src/cli/update-cli.ts +179 -13
- package/src/cli/web-search-cli.ts +5 -7
- package/src/cli.ts +20 -1
- package/src/commands/launch.ts +47 -2
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/commands/ralplan.ts +1 -0
- package/src/commands/telegram.ts +47 -0
- package/src/config/keybindings.ts +15 -6
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-registry.ts +55 -1
- package/src/config/models-config-schema.ts +9 -0
- package/src/config/settings-schema.ts +256 -4
- package/src/config/settings.ts +11 -0
- package/src/config/telegram-autostart.ts +107 -0
- package/src/config/telegram-env-bridge.ts +92 -0
- package/src/coordinator-mcp/server.ts +276 -34
- package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
- package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
- package/src/defaults/skc-defaults.ts +16 -0
- package/src/discovery/builtin.ts +12 -0
- package/src/discovery/cursor.ts +5 -84
- package/src/discovery/gemini.ts +3 -89
- package/src/discovery/index.ts +4 -1
- package/src/discovery/mcp-json.ts +11 -0
- package/src/discovery/opencode.ts +5 -122
- package/src/discovery/windsurf.ts +4 -82
- package/src/edit/read-file.ts +9 -1
- package/src/extensibility/extensions/runner.ts +2 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/skills.ts +30 -2
- package/src/hooks/skill-state.ts +33 -73
- package/src/i18n/messages/de.ts +1 -0
- package/src/i18n/messages/en.ts +1 -0
- package/src/i18n/messages/es.ts +1 -0
- package/src/i18n/messages/fr.ts +1 -0
- package/src/i18n/messages/ja.settings.ts +28 -0
- package/src/i18n/messages/ja.ts +1 -0
- package/src/i18n/messages/ko.settings.ts +28 -0
- package/src/i18n/messages/ko.ts +1 -0
- package/src/i18n/messages/zh.settings.ts +28 -0
- package/src/i18n/messages/zh.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +12 -7
- package/src/main.ts +5 -2
- package/src/migrate/executor.ts +23 -6
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/components/agent-dashboard.ts +3 -35
- package/src/modes/components/custom-editor.ts +26 -0
- package/src/modes/components/custom-model-preset-wizard.ts +30 -189
- package/src/modes/components/extensions/extension-dashboard.ts +2 -48
- package/src/modes/components/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +246 -11
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/session-selector.ts +19 -9
- package/src/modes/components/settings-selector.ts +551 -11
- package/src/modes/components/skill-message.ts +18 -10
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/components/status-line/segments.ts +16 -19
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +281 -117
- package/src/modes/components/tree-selector.ts +28 -8
- package/src/modes/components/two-column-body.ts +36 -0
- package/src/modes/components/welcome.ts +165 -30
- package/src/modes/controllers/command-controller.ts +26 -2
- package/src/modes/controllers/event-controller.ts +102 -4
- package/src/modes/controllers/extension-ui-controller.ts +21 -2
- package/src/modes/controllers/input-controller.ts +214 -47
- package/src/modes/controllers/selector-controller.ts +35 -33
- package/src/modes/interactive-mode.ts +36 -30
- package/src/modes/prompt-action-autocomplete.ts +48 -8
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
- package/src/modes/slash-command-visibility.ts +9 -0
- package/src/modes/theme/theme.ts +5 -1
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/types.ts +8 -1
- package/src/modes/utils/hotkeys-markdown.ts +5 -2
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/modes/utils/ui-helpers.ts +76 -3
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/html-format.ts +75 -1
- package/src/notifications/index.ts +32 -2
- package/src/notifications/lifecycle-control-runtime.ts +12 -3
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-cli.ts +125 -13
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +88 -46
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/agents/architect.md +7 -4
- package/src/prompts/agents/critic.md +15 -6
- package/src/prompts/agents/planner.md +8 -3
- package/src/prompts/system/system-prompt.md +20 -2
- package/src/prompts/tools/bash.md +1 -1
- package/src/runtime-mcp/config-writer.ts +28 -0
- package/src/runtime-mcp/config.ts +7 -0
- package/src/runtime-mcp/loader.ts +3 -0
- package/src/runtime-mcp/manager.ts +3 -0
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk.ts +10 -11
- package/src/session/agent-session.ts +103 -13
- package/src/session/session-manager.ts +58 -18
- package/src/skc-runtime/launch-tmux.ts +34 -8
- package/src/skc-runtime/launch-worktree.ts +16 -2
- package/src/skc-runtime/ralplan-runtime.ts +22 -6
- package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
- package/src/skc-runtime/session-state-sidecar.ts +167 -24
- package/src/skc-runtime/state-runtime.ts +8 -5
- package/src/skc-runtime/state-writer.ts +5 -5
- package/src/skc-runtime/team-runtime.ts +21 -6
- package/src/skc-runtime/tmux-common.ts +16 -0
- package/src/skc-runtime/tmux-sessions.ts +13 -9
- package/src/skc-runtime/ultragoal-guard.ts +150 -24
- package/src/skc-runtime/ultragoal-runtime.ts +16 -124
- package/src/skc-runtime/workflow-manifest.generated.json +0 -8
- package/src/skc-runtime/workflow-manifest.ts +0 -1
- package/src/slash-commands/builtin-registry.ts +136 -23
- package/src/slash-commands/types.ts +2 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +5 -0
- package/src/tools/ast-grep.ts +10 -7
- package/src/tools/bash.ts +39 -14
- package/src/tools/index.ts +8 -1
- package/src/tools/skill.ts +1 -1
- package/src/utils/cmux-workspace.ts +233 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/kagi.ts +1 -1
- package/src/web/parallel.ts +2 -2
- package/src/web/search/index.ts +137 -52
- package/src/web/search/provider.ts +80 -0
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/brave.ts +1 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/gemini.ts +7 -3
- package/src/web/search/providers/jina.ts +1 -1
- package/src/web/search/providers/kimi.ts +6 -1
- package/src/web/search/providers/openai-compatible.ts +1 -1
- package/src/web/search/providers/parallel.ts +1 -1
- package/src/web/search/providers/perplexity.ts +2 -2
- package/src/web/search/providers/searxng.ts +1 -1
- package/src/web/search/providers/synthetic.ts +1 -1
- package/src/web/search/providers/tavily.ts +1 -1
- package/src/web/search/providers/utils.ts +67 -17
- package/src/web/search/providers/xai.ts +1 -1
- package/src/web/search/providers/zai.ts +1 -1
- package/src/web/search/render.ts +3 -0
- package/src/workflow/workflow-intent-diff.ts +199 -0
- package/vendor/insane-search/MANIFEST.json +1 -3
- package/vendor/insane-search/engine/tests/test_u1.py +0 -28
- package/vendor/insane-search/engine/transport.py +2 -3
- package/vendor/insane-search/engine/validators.py +0 -14
- package/src/defaults/skc/rules/ponytail.md +0 -68
|
@@ -26,12 +26,14 @@ import { getPreset } from "./status-line/presets";
|
|
|
26
26
|
import { renderSegment, type SegmentContext } from "./status-line/segments";
|
|
27
27
|
import { getSeparator } from "./status-line/separators";
|
|
28
28
|
import { calculateTokensPerSecond } from "./status-line/token-rate";
|
|
29
|
+
import type { SeparatorDef } from "./status-line/types";
|
|
29
30
|
|
|
30
31
|
export interface StatusLineSegmentOptions {
|
|
31
32
|
model?: { showThinkingLevel?: boolean };
|
|
32
33
|
path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
|
|
33
34
|
git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
|
|
34
35
|
time?: { format?: "12h" | "24h"; showSeconds?: boolean };
|
|
36
|
+
usage?: { mode?: "used" | "remaining" };
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface StatusLineSettings {
|
|
@@ -40,15 +42,34 @@ export interface StatusLineSettings {
|
|
|
40
42
|
rightSegments?: StatusLineSegmentId[];
|
|
41
43
|
separator?: StatusLineSeparatorStyle;
|
|
42
44
|
segmentOptions?: StatusLineSegmentOptions;
|
|
45
|
+
previewHighlightSegment?: StatusLineSegmentId;
|
|
43
46
|
showHookStatus?: boolean;
|
|
44
47
|
showSkillHud?: boolean;
|
|
45
48
|
sessionAccent?: boolean;
|
|
49
|
+
maxRows?: number;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
export interface StatusLineComponentOptions {
|
|
49
53
|
version?: string;
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
interface CollectedStatusSegments {
|
|
57
|
+
ctx: SegmentContext;
|
|
58
|
+
separatorDef: SeparatorDef;
|
|
59
|
+
bgAnsi: string;
|
|
60
|
+
fgAnsi: string;
|
|
61
|
+
sepAnsi: string;
|
|
62
|
+
left: string[];
|
|
63
|
+
leftSegIds: StatusLineSegmentId[];
|
|
64
|
+
right: string[];
|
|
65
|
+
previewHighlightSegment: StatusLineSegmentId | undefined;
|
|
66
|
+
sessionAccent: boolean | undefined;
|
|
67
|
+
leftSepWidth: number;
|
|
68
|
+
rightSepWidth: number;
|
|
69
|
+
leftCapWidth: number;
|
|
70
|
+
rightCapWidth: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
53
74
|
// Per-message token cache
|
|
54
75
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -181,11 +202,8 @@ export class StatusLineComponent implements Component {
|
|
|
181
202
|
#lastTokensPerSecond: number | null = null;
|
|
182
203
|
#lastTokensPerSecondTimestamp: number | null = null;
|
|
183
204
|
|
|
184
|
-
//
|
|
185
|
-
#cachedUsage:
|
|
186
|
-
fiveHour?: { percent: number; resetMinutes?: number };
|
|
187
|
-
sevenDay?: { percent: number; resetHours?: number };
|
|
188
|
-
} | null = null;
|
|
205
|
+
// Provider usage caching (5-min TTL, OAuth/sub only)
|
|
206
|
+
#cachedUsage: SegmentContext["usage"] = null;
|
|
189
207
|
#usageFetchedAt = 0;
|
|
190
208
|
#usageInFlight = false;
|
|
191
209
|
// Context breakdown — incremental cache. Replaces the previous 2-second
|
|
@@ -216,12 +234,13 @@ export class StatusLineComponent implements Component {
|
|
|
216
234
|
showSkillHud: settings.get("statusLine.showSkillHud"),
|
|
217
235
|
segmentOptions: settings.getGroup("statusLine").segmentOptions,
|
|
218
236
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
237
|
+
maxRows: settings.get("statusLine.maxRows"),
|
|
219
238
|
};
|
|
220
239
|
this.#version = options.version?.trim() || undefined;
|
|
221
240
|
}
|
|
222
241
|
|
|
223
242
|
updateSettings(settings: StatusLineSettings): void {
|
|
224
|
-
this.#settings = { ...this.#settings, ...settings };
|
|
243
|
+
this.#settings = { ...this.#settings, previewHighlightSegment: undefined, ...settings };
|
|
225
244
|
}
|
|
226
245
|
|
|
227
246
|
setAutoCompactEnabled(enabled: boolean): void {
|
|
@@ -462,9 +481,9 @@ export class StatusLineComponent implements Component {
|
|
|
462
481
|
}
|
|
463
482
|
|
|
464
483
|
/**
|
|
465
|
-
* Background-refresh the
|
|
466
|
-
*
|
|
467
|
-
*
|
|
484
|
+
* Background-refresh the OAuth quota report. Guarded by a 5-min TTL on both
|
|
485
|
+
* success (cache lifetime) and error (backoff). Exposed (non-private) so
|
|
486
|
+
* unit tests can verify the backoff invariant.
|
|
468
487
|
*/
|
|
469
488
|
refreshUsageInBackground(): void {
|
|
470
489
|
const now = Date.now();
|
|
@@ -478,6 +497,9 @@ export class StatusLineComponent implements Component {
|
|
|
478
497
|
.then(reports => {
|
|
479
498
|
this.#cachedUsage = this.#normalizeUsageReports(reports);
|
|
480
499
|
this.#usageFetchedAt = Date.now();
|
|
500
|
+
if (this.#onBranchChange) {
|
|
501
|
+
this.#onBranchChange();
|
|
502
|
+
}
|
|
481
503
|
})
|
|
482
504
|
.catch(() => {
|
|
483
505
|
// Backoff on error: stamp the fetch time so the 5-min TTL guard
|
|
@@ -491,47 +513,77 @@ export class StatusLineComponent implements Component {
|
|
|
491
513
|
});
|
|
492
514
|
}
|
|
493
515
|
|
|
494
|
-
#normalizeUsageReports(reports: unknown): {
|
|
495
|
-
fiveHour?: { percent: number; resetMinutes?: number };
|
|
496
|
-
sevenDay?: { percent: number; resetHours?: number };
|
|
497
|
-
} | null {
|
|
516
|
+
#normalizeUsageReports(reports: unknown): SegmentContext["usage"] {
|
|
498
517
|
if (!Array.isArray(reports)) return null;
|
|
499
|
-
|
|
500
|
-
|
|
518
|
+
const windows: NonNullable<SegmentContext["usage"]>["windows"] = [];
|
|
519
|
+
const seen = new Set<string>();
|
|
501
520
|
const now = Date.now();
|
|
521
|
+
|
|
522
|
+
const codexWindowLabel = (windowId: string | undefined, fallback: string): string => {
|
|
523
|
+
if (windowId && /^\d+[hd]$/.test(windowId)) return windowId;
|
|
524
|
+
return fallback;
|
|
525
|
+
};
|
|
526
|
+
const codexResetUnit = (label: string): "m" | "h" => (label.endsWith("d") ? "h" : "m");
|
|
527
|
+
|
|
528
|
+
const pushWindow = (
|
|
529
|
+
key: string,
|
|
530
|
+
label: string,
|
|
531
|
+
fraction: number,
|
|
532
|
+
resetsAt: number | undefined,
|
|
533
|
+
resetUnit: "m" | "h",
|
|
534
|
+
) => {
|
|
535
|
+
if (seen.has(key)) return;
|
|
536
|
+
seen.add(key);
|
|
537
|
+
windows.push({
|
|
538
|
+
label,
|
|
539
|
+
percent: fraction * 100,
|
|
540
|
+
resetValue:
|
|
541
|
+
typeof resetsAt === "number"
|
|
542
|
+
? Math.max(0, Math.round((resetsAt - now) / (resetUnit === "m" ? 60_000 : 3_600_000)))
|
|
543
|
+
: undefined,
|
|
544
|
+
resetUnit,
|
|
545
|
+
});
|
|
546
|
+
};
|
|
547
|
+
|
|
502
548
|
for (const report of reports) {
|
|
503
549
|
if (!report || typeof report !== "object") continue;
|
|
550
|
+
const provider = (report as { provider?: unknown }).provider;
|
|
551
|
+
const providerId = typeof provider === "string" ? provider : undefined;
|
|
504
552
|
const limits = (report as { limits?: unknown }).limits;
|
|
505
553
|
if (!Array.isArray(limits)) continue;
|
|
506
554
|
for (const limit of limits) {
|
|
507
555
|
if (!limit || typeof limit !== "object") continue;
|
|
508
556
|
const l = limit as {
|
|
509
|
-
|
|
510
|
-
|
|
557
|
+
id?: unknown;
|
|
558
|
+
scope?: { windowId?: string; tier?: string; modelId?: string };
|
|
559
|
+
window?: { id?: string; resetsAt?: number };
|
|
511
560
|
amount?: { usedFraction?: number };
|
|
512
561
|
};
|
|
513
562
|
const fraction = l.amount?.usedFraction;
|
|
514
563
|
if (typeof fraction !== "number") continue;
|
|
515
|
-
const
|
|
564
|
+
const id = typeof l.id === "string" ? l.id : "";
|
|
565
|
+
const windowId = l.scope?.windowId ?? l.window?.id;
|
|
516
566
|
const tier = l.scope?.tier;
|
|
567
|
+
const modelId = l.scope?.modelId;
|
|
517
568
|
const resetsAt = l.window?.resetsAt;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
569
|
+
|
|
570
|
+
if (providerId === "openai-codex") {
|
|
571
|
+
if (id === "openai-codex:primary" || (!id && !!windowId && windowId !== "7d" && !modelId)) {
|
|
572
|
+
const label = codexWindowLabel(windowId, "primary");
|
|
573
|
+
pushWindow("codex:primary", label, fraction, resetsAt, codexResetUnit(label));
|
|
574
|
+
} else if (id === "openai-codex:secondary" || (!id && windowId === "7d" && !modelId)) {
|
|
575
|
+
const label = codexWindowLabel(windowId, "secondary");
|
|
576
|
+
pushWindow("codex:secondary", label, fraction, resetsAt, codexResetUnit(label));
|
|
577
|
+
}
|
|
578
|
+
} else if (windowId === "5h" && !tier) {
|
|
579
|
+
pushWindow(`${providerId ?? "provider"}:5h`, "5h", fraction, resetsAt, "m");
|
|
580
|
+
} else if (windowId === "7d" && !tier) {
|
|
581
|
+
pushWindow(`${providerId ?? "provider"}:7d`, "7d", fraction, resetsAt, "h");
|
|
530
582
|
}
|
|
531
583
|
}
|
|
532
584
|
}
|
|
533
|
-
|
|
534
|
-
return {
|
|
585
|
+
|
|
586
|
+
return windows.length > 0 ? { windows } : null;
|
|
535
587
|
}
|
|
536
588
|
|
|
537
589
|
/**
|
|
@@ -670,7 +722,70 @@ export class StatusLineComponent implements Component {
|
|
|
670
722
|
};
|
|
671
723
|
}
|
|
672
724
|
|
|
673
|
-
#
|
|
725
|
+
#groupWidth(parts: string[], capWidth: number, sepWidth: number): number {
|
|
726
|
+
if (parts.length === 0) return 0;
|
|
727
|
+
const partsWidth = parts.reduce((sum, part) => sum + visibleWidth(part), 0);
|
|
728
|
+
const sepTotal = Math.max(0, parts.length - 1) * (sepWidth + 2);
|
|
729
|
+
return partsWidth + sepTotal + 2 + capWidth;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
#renderStatusGroup(
|
|
733
|
+
parts: string[],
|
|
734
|
+
direction: "left" | "right",
|
|
735
|
+
separatorDef: SeparatorDef,
|
|
736
|
+
bgAnsi: string,
|
|
737
|
+
fgAnsi: string,
|
|
738
|
+
sepAnsi: string,
|
|
739
|
+
): string {
|
|
740
|
+
if (parts.length === 0) return "";
|
|
741
|
+
const sep = direction === "left" ? separatorDef.left : separatorDef.right;
|
|
742
|
+
const cap = separatorDef.endCaps
|
|
743
|
+
? direction === "left"
|
|
744
|
+
? separatorDef.endCaps.right
|
|
745
|
+
: separatorDef.endCaps.left
|
|
746
|
+
: "";
|
|
747
|
+
const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : bgAnsi + sepAnsi;
|
|
748
|
+
const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
|
|
749
|
+
|
|
750
|
+
let content = bgAnsi + fgAnsi;
|
|
751
|
+
content += ` ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} `;
|
|
752
|
+
content += "\x1b[0m";
|
|
753
|
+
|
|
754
|
+
if (capText) {
|
|
755
|
+
return direction === "right" ? capText + content : content + capText;
|
|
756
|
+
}
|
|
757
|
+
return content;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
#shrinkPathToWidth(content: string, ctx: SegmentContext, shrinkBy: number): string | null {
|
|
761
|
+
const currentPathVW = visibleWidth(content);
|
|
762
|
+
const minPathVW = 8; // icon + ellipsis + a few chars
|
|
763
|
+
const shrinkable = currentPathVW - minPathVW;
|
|
764
|
+
if (shrinkable <= 0 || shrinkBy <= 0) return null;
|
|
765
|
+
const targetShrink = Math.min(shrinkable, shrinkBy);
|
|
766
|
+
const currentMaxLen = ctx.options.path?.maxLength ?? 40;
|
|
767
|
+
let newMaxLen = Math.max(4, Math.min(currentMaxLen, currentPathVW) - targetShrink);
|
|
768
|
+
const pathCtx = (maxLen: number): SegmentContext => ({
|
|
769
|
+
...ctx,
|
|
770
|
+
options: { ...ctx.options, path: { ...ctx.options.path, maxLength: maxLen } },
|
|
771
|
+
});
|
|
772
|
+
let reRendered = renderSegment("path", pathCtx(newMaxLen));
|
|
773
|
+
if (!reRendered.visible || !reRendered.content) return null;
|
|
774
|
+
// maxLength governs path text, not icon prefix; iterate to compensate.
|
|
775
|
+
for (let i = 0; i < 8; i++) {
|
|
776
|
+
const saved = currentPathVW - visibleWidth(reRendered.content);
|
|
777
|
+
if (saved >= targetShrink) break;
|
|
778
|
+
const nextMaxLen = Math.max(4, newMaxLen - (targetShrink - saved));
|
|
779
|
+
if (nextMaxLen >= newMaxLen) break; // no progress or hit floor
|
|
780
|
+
newMaxLen = nextMaxLen;
|
|
781
|
+
const adjusted = renderSegment("path", pathCtx(newMaxLen));
|
|
782
|
+
if (!adjusted.visible || !adjusted.content) break;
|
|
783
|
+
reRendered = adjusted;
|
|
784
|
+
}
|
|
785
|
+
return reRendered.content;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
#collectStatusSegments(width: number): CollectedStatusSegments {
|
|
674
789
|
const ctx = this.#buildSegmentContext(width);
|
|
675
790
|
const effectiveSettings = this.#resolveSettings();
|
|
676
791
|
const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
|
|
@@ -683,22 +798,25 @@ export class StatusLineComponent implements Component {
|
|
|
683
798
|
const fgAnsi = theme.getFgAnsi("text");
|
|
684
799
|
const sepAnsi = theme.getFgAnsi("statusLineSep");
|
|
685
800
|
|
|
686
|
-
|
|
687
|
-
const
|
|
801
|
+
const previewHighlightSegment = effectiveSettings.previewHighlightSegment;
|
|
802
|
+
const highlightSegment = (segId: StatusLineSegmentId, content: string): string =>
|
|
803
|
+
previewHighlightSegment === segId ? `\x1b[7m${content}\x1b[27m` : content;
|
|
804
|
+
|
|
805
|
+
const left: string[] = [];
|
|
688
806
|
const leftSegIds: StatusLineSegmentId[] = [];
|
|
689
807
|
for (const segId of effectiveSettings.leftSegments) {
|
|
690
808
|
const rendered = renderSegment(segId, ctx);
|
|
691
809
|
if (rendered.visible && rendered.content) {
|
|
692
|
-
|
|
810
|
+
left.push(highlightSegment(segId, rendered.content));
|
|
693
811
|
leftSegIds.push(segId);
|
|
694
812
|
}
|
|
695
813
|
}
|
|
696
814
|
|
|
697
|
-
const
|
|
815
|
+
const right: string[] = [];
|
|
698
816
|
for (const segId of effectiveSettings.rightSegments) {
|
|
699
817
|
const rendered = renderSegment(segId, ctx);
|
|
700
818
|
if (rendered.visible && rendered.content) {
|
|
701
|
-
|
|
819
|
+
right.push(highlightSegment(segId, rendered.content));
|
|
702
820
|
}
|
|
703
821
|
}
|
|
704
822
|
|
|
@@ -707,116 +825,152 @@ export class StatusLineComponent implements Component {
|
|
|
707
825
|
if (runningBackgroundJobs > 0) {
|
|
708
826
|
const icon = theme.icon.agents ? `${theme.icon.agents} ` : "";
|
|
709
827
|
const label = `${formatCount("job", runningBackgroundJobs)} running`;
|
|
710
|
-
|
|
828
|
+
right.push(theme.fg("statusLineSubagents", `${icon}${label}`));
|
|
711
829
|
}
|
|
712
830
|
if (this.#version) {
|
|
713
|
-
|
|
831
|
+
right.push(theme.fg("dim", `v${this.#version}`));
|
|
714
832
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
833
|
+
|
|
834
|
+
return {
|
|
835
|
+
ctx,
|
|
836
|
+
separatorDef,
|
|
837
|
+
bgAnsi,
|
|
838
|
+
fgAnsi,
|
|
839
|
+
sepAnsi,
|
|
840
|
+
left,
|
|
841
|
+
leftSegIds,
|
|
842
|
+
right,
|
|
843
|
+
previewHighlightSegment,
|
|
844
|
+
sessionAccent: effectiveSettings.sessionAccent,
|
|
845
|
+
leftSepWidth: visibleWidth(separatorDef.left),
|
|
846
|
+
rightSepWidth: visibleWidth(separatorDef.right),
|
|
847
|
+
leftCapWidth: separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.right) : 0,
|
|
848
|
+
rightCapWidth: separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.left) : 0,
|
|
729
849
|
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
#resolveMaxRows(): number {
|
|
853
|
+
const raw = this.#settings.maxRows ?? 1;
|
|
854
|
+
if (!Number.isFinite(raw)) return 1;
|
|
855
|
+
return Math.max(1, Math.min(3, Math.trunc(raw)));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
#buildStatusLine(width: number, precollected?: CollectedStatusSegments): string {
|
|
859
|
+
const seg = precollected ?? this.#collectStatusSegments(width);
|
|
860
|
+
const { ctx, separatorDef, bgAnsi, fgAnsi, sepAnsi, previewHighlightSegment } = seg;
|
|
861
|
+
const { leftSepWidth, rightSepWidth, leftCapWidth, rightCapWidth } = seg;
|
|
862
|
+
const left = [...seg.left];
|
|
863
|
+
const leftIds = [...seg.leftSegIds];
|
|
864
|
+
const right = [...seg.right];
|
|
865
|
+
const topFillWidth = Math.max(0, width);
|
|
730
866
|
|
|
731
|
-
let leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
732
|
-
let rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
|
|
867
|
+
let leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
868
|
+
let rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
733
869
|
const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
|
|
734
870
|
|
|
735
871
|
if (topFillWidth > 0) {
|
|
736
872
|
while (totalWidth() > topFillWidth && right.length > 0) {
|
|
737
873
|
right.pop();
|
|
738
|
-
rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
|
|
874
|
+
rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
739
875
|
}
|
|
740
876
|
// Shrink path before dropping left segments — path is the only elastic segment
|
|
741
|
-
const pathIdx =
|
|
877
|
+
const pathIdx = leftIds.indexOf("path");
|
|
742
878
|
if (pathIdx >= 0 && totalWidth() > topFillWidth) {
|
|
743
879
|
const overflow = totalWidth() - topFillWidth;
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const shrinkBy = Math.min(shrinkable, overflow);
|
|
749
|
-
const currentMaxLen = ctx.options.path?.maxLength ?? 40;
|
|
750
|
-
let newMaxLen = Math.max(4, Math.min(currentMaxLen, currentPathVW) - shrinkBy);
|
|
751
|
-
const pathCtx = (maxLen: number): SegmentContext => ({
|
|
752
|
-
...ctx,
|
|
753
|
-
options: { ...ctx.options, path: { ...ctx.options.path, maxLength: maxLen } },
|
|
754
|
-
});
|
|
755
|
-
let reRendered = renderSegment("path", pathCtx(newMaxLen));
|
|
756
|
-
if (reRendered.visible && reRendered.content) {
|
|
757
|
-
// maxLength governs path text, not icon prefix; iterate to compensate
|
|
758
|
-
for (let i = 0; i < 8; i++) {
|
|
759
|
-
const saved = currentPathVW - visibleWidth(reRendered.content);
|
|
760
|
-
if (saved >= shrinkBy) break;
|
|
761
|
-
const nextMaxLen = Math.max(4, newMaxLen - (shrinkBy - saved));
|
|
762
|
-
if (nextMaxLen >= newMaxLen) break; // no progress or hit floor
|
|
763
|
-
newMaxLen = nextMaxLen;
|
|
764
|
-
const adjusted = renderSegment("path", pathCtx(newMaxLen));
|
|
765
|
-
if (!adjusted.visible || !adjusted.content) break;
|
|
766
|
-
reRendered = adjusted;
|
|
767
|
-
}
|
|
768
|
-
left[pathIdx] = reRendered.content;
|
|
769
|
-
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
770
|
-
}
|
|
880
|
+
const shrunk = this.#shrinkPathToWidth(left[pathIdx], ctx, overflow);
|
|
881
|
+
if (shrunk !== null) {
|
|
882
|
+
left[pathIdx] = previewHighlightSegment === "path" ? `\x1b[7m${shrunk}\x1b[27m` : shrunk;
|
|
883
|
+
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
771
884
|
}
|
|
772
885
|
}
|
|
773
886
|
while (totalWidth() > topFillWidth && left.length > 0) {
|
|
774
887
|
left.pop();
|
|
775
|
-
|
|
776
|
-
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
888
|
+
leftIds.pop();
|
|
889
|
+
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
777
890
|
}
|
|
778
891
|
}
|
|
779
892
|
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
const sep = direction === "left" ? separatorDef.left : separatorDef.right;
|
|
783
|
-
const cap = separatorDef.endCaps
|
|
784
|
-
? direction === "left"
|
|
785
|
-
? separatorDef.endCaps.right
|
|
786
|
-
: separatorDef.endCaps.left
|
|
787
|
-
: "";
|
|
788
|
-
const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : bgAnsi + sepAnsi;
|
|
789
|
-
const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
|
|
790
|
-
|
|
791
|
-
let content = bgAnsi + fgAnsi;
|
|
792
|
-
content += ` ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} `;
|
|
793
|
-
content += "\x1b[0m";
|
|
794
|
-
|
|
795
|
-
if (capText) {
|
|
796
|
-
return direction === "right" ? capText + content : content + capText;
|
|
797
|
-
}
|
|
798
|
-
return content;
|
|
799
|
-
};
|
|
800
|
-
|
|
801
|
-
const leftGroup = renderGroup(left, "left");
|
|
802
|
-
const rightGroup = renderGroup(right, "right");
|
|
893
|
+
const leftGroup = this.#renderStatusGroup(left, "left", separatorDef, bgAnsi, fgAnsi, sepAnsi);
|
|
894
|
+
const rightGroup = this.#renderStatusGroup(right, "right", separatorDef, bgAnsi, fgAnsi, sepAnsi);
|
|
803
895
|
if (!leftGroup && !rightGroup) return "";
|
|
804
896
|
|
|
805
897
|
if (topFillWidth === 0 || left.length === 0 || right.length === 0) {
|
|
806
898
|
return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
|
|
807
899
|
}
|
|
808
900
|
|
|
809
|
-
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
810
|
-
rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
|
|
901
|
+
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
902
|
+
rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
811
903
|
const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
|
|
812
|
-
const sessionName =
|
|
813
|
-
effectiveSettings.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
|
|
904
|
+
const sessionName = seg.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
|
|
814
905
|
const accentHex = sessionName ? getSessionAccentHex(sessionName) : undefined;
|
|
815
906
|
const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
|
|
816
907
|
const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
|
|
817
908
|
return leftGroup + gapFill + rightGroup;
|
|
818
909
|
}
|
|
819
910
|
|
|
911
|
+
/**
|
|
912
|
+
* Multi-row status line. When `maxRows > 1` and the single-line layout would
|
|
913
|
+
* overflow, segments wrap onto additional left-justified rows instead of
|
|
914
|
+
* being dropped. Falls back to the polished justified single row whenever
|
|
915
|
+
* everything fits on one line.
|
|
916
|
+
*/
|
|
917
|
+
#buildStatusRows(width: number, maxRows: number): string[] {
|
|
918
|
+
const seg = this.#collectStatusSegments(width);
|
|
919
|
+
if (seg.left.length === 0 && seg.right.length === 0) return [];
|
|
920
|
+
|
|
921
|
+
const topFillWidth = Math.max(1, width);
|
|
922
|
+
const leftWidth = this.#groupWidth(seg.left, seg.leftCapWidth, seg.leftSepWidth);
|
|
923
|
+
const rightWidth = this.#groupWidth(seg.right, seg.rightCapWidth, seg.rightSepWidth);
|
|
924
|
+
const gap = seg.left.length > 0 && seg.right.length > 0 ? 1 : 0;
|
|
925
|
+
const fitsSingleRow = leftWidth + rightWidth + gap <= topFillWidth;
|
|
926
|
+
|
|
927
|
+
if (maxRows <= 1 || fitsSingleRow) {
|
|
928
|
+
const single = this.#buildStatusLine(width, seg);
|
|
929
|
+
return single ? [single] : [];
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Ordered sequence: left segments first (in order), then right segments.
|
|
933
|
+
const items: { content: string; isPath: boolean }[] = [
|
|
934
|
+
...seg.left.map((content, i) => ({ content, isPath: seg.leftSegIds[i] === "path" })),
|
|
935
|
+
...seg.right.map(content => ({ content, isPath: false })),
|
|
936
|
+
];
|
|
937
|
+
|
|
938
|
+
// Pre-shrink an over-wide path so it never blows out a row on its own.
|
|
939
|
+
for (const item of items) {
|
|
940
|
+
if (!item.isPath) continue;
|
|
941
|
+
const alone = this.#groupWidth([item.content], seg.leftCapWidth, seg.leftSepWidth);
|
|
942
|
+
if (alone > topFillWidth) {
|
|
943
|
+
const shrunk = this.#shrinkPathToWidth(item.content, seg.ctx, alone - topFillWidth);
|
|
944
|
+
if (shrunk !== null) item.content = shrunk;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Greedy pack into up to `maxRows` left-justified rows.
|
|
949
|
+
const rows: string[][] = [];
|
|
950
|
+
let current: string[] = [];
|
|
951
|
+
for (const item of items) {
|
|
952
|
+
if (current.length === 0) {
|
|
953
|
+
current.push(item.content);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const tentative = [...current, item.content];
|
|
957
|
+
if (this.#groupWidth(tentative, seg.leftCapWidth, seg.leftSepWidth) <= topFillWidth) {
|
|
958
|
+
current = tentative;
|
|
959
|
+
} else {
|
|
960
|
+
rows.push(current);
|
|
961
|
+
current = [item.content];
|
|
962
|
+
if (rows.length >= maxRows) break;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (rows.length < maxRows && current.length > 0) {
|
|
966
|
+
rows.push(current);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return rows.map(row =>
|
|
970
|
+
this.#renderStatusGroup(row, "left", seg.separatorDef, seg.bgAnsi, seg.fgAnsi, seg.sepAnsi),
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
820
974
|
getTopBorder(width: number): { content: string; width: number } {
|
|
821
975
|
const content = this.#buildStatusLine(width);
|
|
822
976
|
return {
|
|
@@ -825,6 +979,16 @@ export class StatusLineComponent implements Component {
|
|
|
825
979
|
};
|
|
826
980
|
}
|
|
827
981
|
|
|
982
|
+
/**
|
|
983
|
+
* Multi-row-aware content for the settings preview: the wrapped rows joined
|
|
984
|
+
* with newlines so a single `Text` renders them stacked. Honors the current
|
|
985
|
+
* `maxRows`; identical to the single status line when `maxRows` is 1 or when
|
|
986
|
+
* everything fits on one row.
|
|
987
|
+
*/
|
|
988
|
+
getPreviewContent(width: number): string {
|
|
989
|
+
return this.#buildStatusRows(width, this.#resolveMaxRows()).join("\n");
|
|
990
|
+
}
|
|
991
|
+
|
|
828
992
|
render(width: number): string[] {
|
|
829
993
|
const lines: string[] = [];
|
|
830
994
|
this.#refreshSkillHudInBackground();
|
|
@@ -833,9 +997,9 @@ export class StatusLineComponent implements Component {
|
|
|
833
997
|
lines.push(skillHud);
|
|
834
998
|
}
|
|
835
999
|
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
lines.push(truncateToWidth(
|
|
1000
|
+
const statusRows = this.#buildStatusRows(width, this.#resolveMaxRows());
|
|
1001
|
+
for (const statusRow of statusRows) {
|
|
1002
|
+
if (statusRow) lines.push(truncateToWidth(statusRow, width));
|
|
839
1003
|
}
|
|
840
1004
|
|
|
841
1005
|
const showHooks = this.#settings.showHookStatus ?? true;
|
|
@@ -135,6 +135,14 @@ class TreeList implements Component {
|
|
|
135
135
|
return this.#filteredNodes.length - 1;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
#clampSelectedIndex(): void {
|
|
139
|
+
if (this.#filteredNodes.length === 0) {
|
|
140
|
+
this.#selectedIndex = 0;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.#selectedIndex = Math.max(0, Math.min(this.#selectedIndex, this.#filteredNodes.length - 1));
|
|
144
|
+
}
|
|
145
|
+
|
|
138
146
|
#flattenTree(roots: SessionTreeNode[]): FlatNode[] {
|
|
139
147
|
const result: FlatNode[] = [];
|
|
140
148
|
this.#toolCallMap.clear();
|
|
@@ -333,12 +341,11 @@ class TreeList implements Component {
|
|
|
333
341
|
return true;
|
|
334
342
|
});
|
|
335
343
|
|
|
336
|
-
// Try to preserve cursor on the same node, or
|
|
344
|
+
// Try to preserve cursor on the same node, or keep the index in range.
|
|
337
345
|
if (this.#lastSelectedId) {
|
|
338
346
|
this.#selectedIndex = this.#findNearestVisibleIndex(this.#lastSelectedId);
|
|
339
|
-
} else
|
|
340
|
-
|
|
341
|
-
this.#selectedIndex = Math.max(0, this.#filteredNodes.length - 1);
|
|
347
|
+
} else {
|
|
348
|
+
this.#clampSelectedIndex();
|
|
342
349
|
}
|
|
343
350
|
|
|
344
351
|
// Update lastSelectedId to the actual selection (may have changed due to parent walk)
|
|
@@ -710,16 +717,29 @@ class TreeList implements Component {
|
|
|
710
717
|
}
|
|
711
718
|
|
|
712
719
|
handleInput(keyData: string): void {
|
|
720
|
+
const moveSelection = (delta: number, wrap: boolean): void => {
|
|
721
|
+
if (this.#filteredNodes.length === 0) return;
|
|
722
|
+
if (wrap) {
|
|
723
|
+
const next = this.#selectedIndex + delta;
|
|
724
|
+
this.#selectedIndex =
|
|
725
|
+
next < 0 ? this.#filteredNodes.length - 1 : next >= this.#filteredNodes.length ? 0 : next;
|
|
726
|
+
} else {
|
|
727
|
+
this.#selectedIndex += delta;
|
|
728
|
+
this.#clampSelectedIndex();
|
|
729
|
+
}
|
|
730
|
+
this.#lastSelectedId = this.#filteredNodes[this.#selectedIndex]?.node.entry.id ?? this.#lastSelectedId;
|
|
731
|
+
};
|
|
732
|
+
|
|
713
733
|
if (matchesKey(keyData, "up")) {
|
|
714
|
-
|
|
734
|
+
moveSelection(-1, true);
|
|
715
735
|
} else if (matchesKey(keyData, "down")) {
|
|
716
|
-
|
|
736
|
+
moveSelection(1, true);
|
|
717
737
|
} else if (matchesKey(keyData, "left")) {
|
|
718
738
|
// Page up
|
|
719
|
-
|
|
739
|
+
moveSelection(-this.maxVisibleLines, false);
|
|
720
740
|
} else if (matchesKey(keyData, "right")) {
|
|
721
741
|
// Page down
|
|
722
|
-
|
|
742
|
+
moveSelection(this.maxVisibleLines, false);
|
|
723
743
|
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
724
744
|
const selected = this.#filteredNodes[this.#selectedIndex];
|
|
725
745
|
if (selected && this.onSelect) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Component, padding, truncateToWidth, visibleWidth } from "@sayknow-cli/tui";
|
|
2
|
+
import { theme } from "../theme/theme";
|
|
3
|
+
|
|
4
|
+
export class TwoColumnBody implements Component {
|
|
5
|
+
constructor(
|
|
6
|
+
private readonly leftPane: Component,
|
|
7
|
+
private readonly rightPane: Component,
|
|
8
|
+
private readonly maxHeight: number,
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
render(width: number): string[] {
|
|
12
|
+
const leftWidth = Math.floor(width * 0.5);
|
|
13
|
+
const separatorText = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
|
14
|
+
const separatorWidth = width - leftWidth > 3 ? visibleWidth(separatorText) : 0;
|
|
15
|
+
const separator = separatorWidth > 0 ? separatorText : "";
|
|
16
|
+
const rightWidth = Math.max(0, width - leftWidth - separatorWidth);
|
|
17
|
+
const leftLines = this.leftPane.render(leftWidth);
|
|
18
|
+
const rightLines = this.rightPane.render(rightWidth);
|
|
19
|
+
const lineCount = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
|
|
20
|
+
const out: string[] = [];
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < lineCount; i++) {
|
|
23
|
+
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
|
24
|
+
const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
|
|
25
|
+
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
|
|
26
|
+
out.push(leftPadded + separator + right);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
invalidate(): void {
|
|
33
|
+
this.leftPane.invalidate?.();
|
|
34
|
+
this.rightPane.invalidate?.();
|
|
35
|
+
}
|
|
36
|
+
}
|