dsh-ssh-tui 0.5.2 → 0.5.4

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 (49) hide show
  1. package/README.en.md +36 -6
  2. package/README.md +24 -13
  3. package/lib/approval-reviewer.js +102 -17
  4. package/lib/approval-reviewer.js.map +1 -1
  5. package/lib/auto-approval.js +312 -29
  6. package/lib/auto-approval.js.map +1 -1
  7. package/lib/footer.js +337 -0
  8. package/lib/footer.js.map +1 -0
  9. package/lib/i18n/en.js +83 -8
  10. package/lib/i18n/en.js.map +1 -1
  11. package/lib/i18n/index.js +1 -0
  12. package/lib/i18n/index.js.map +1 -1
  13. package/lib/i18n/zh.js +83 -8
  14. package/lib/i18n/zh.js.map +1 -1
  15. package/lib/json-args.js +30 -0
  16. package/lib/json-args.js.map +1 -0
  17. package/lib/paint.js +262 -0
  18. package/lib/paint.js.map +1 -0
  19. package/lib/picker.js +407 -56
  20. package/lib/picker.js.map +1 -1
  21. package/lib/plan.js +369 -0
  22. package/lib/plan.js.map +1 -0
  23. package/lib/quota.js +408 -0
  24. package/lib/quota.js.map +1 -0
  25. package/lib/session-list.js +18 -21
  26. package/lib/session-list.js.map +1 -1
  27. package/lib/term-text.js +827 -0
  28. package/lib/term-text.js.map +1 -0
  29. package/lib/tool-present.js +744 -0
  30. package/lib/tool-present.js.map +1 -0
  31. package/lib/transcript-types.js +6 -0
  32. package/lib/transcript-types.js.map +1 -0
  33. package/lib/tui.js +747 -3077
  34. package/lib/tui.js.map +1 -1
  35. package/lib/types/approval-reviewer.d.ts +18 -4
  36. package/lib/types/auto-approval.d.ts +53 -2
  37. package/lib/types/footer.d.ts +155 -0
  38. package/lib/types/i18n/index.d.ts +2 -0
  39. package/lib/types/json-args.d.ts +7 -0
  40. package/lib/types/paint.d.ts +78 -0
  41. package/lib/types/picker.d.ts +99 -7
  42. package/lib/types/plan.d.ts +80 -0
  43. package/lib/types/quota.d.ts +94 -0
  44. package/lib/types/session-list.d.ts +13 -0
  45. package/lib/types/term-text.d.ts +130 -0
  46. package/lib/types/tool-present.d.ts +165 -0
  47. package/lib/types/transcript-types.d.ts +152 -0
  48. package/lib/types/tui.d.ts +39 -665
  49. package/package.json +1 -1
@@ -1,8 +1,23 @@
1
+ /**
2
+ * AI reviewer for /approval auto mode: commands whose shape the rule table
3
+ * cannot classify are judged by the subagent-configured model with compact
4
+ * context (latest user message, recent model output, the pending tool call)
5
+ * instead of paging the human.
6
+ *
7
+ * Injection hardening: everything inside the marked data regions is review
8
+ * MATERIAL, never instructions — the system prompt says so, and material that
9
+ * tries to instruct is treated as an injection attempt and rejected. The
10
+ * caller also enforces its own floor: `high` risk or `authorization` other
11
+ * than `yes` is never approved, whatever the model claims.
12
+ */
1
13
  export interface ReviewInput {
2
14
  userText: string;
3
15
  segments: string[];
4
16
  toolName: string;
5
17
  command: string;
18
+ args?: string;
19
+ reason?: string;
20
+ sandboxMode?: string;
6
21
  }
7
22
  export interface ReviewVerdict {
8
23
  risk: 'low' | 'medium' | 'high';
@@ -10,11 +25,10 @@ export interface ReviewVerdict {
10
25
  approved: boolean;
11
26
  reason: string;
12
27
  }
28
+ export type ReviewerLocale = 'zh' | 'en';
29
+ /** Default (zh) prompt; tests and callers that do not pass a locale use this. */
13
30
  export declare const REVIEW_SYSTEM_PROMPT: string;
31
+ export declare function reviewSystemPrompt(locale?: ReviewerLocale): string;
14
32
  /** Assemble the compact, fence-marked user message for the reviewer. */
15
33
  export declare function buildReviewUserMessage(input: ReviewInput): string;
16
- /**
17
- * Parse the reviewer's one-line JSON verdict. Anything unreadable, missing
18
- * fields, or with invalid enum values returns undefined (fail-safe).
19
- */
20
34
  export declare function parseReviewOutput(text: string): ReviewVerdict | undefined;
@@ -11,32 +11,83 @@
11
11
  */
12
12
  export type AutoApprovalMode = 'off' | 'auto';
13
13
  export type ApprovalDecision = 'allow' | 'deny' | 'ask';
14
+ export type AutoApprovalRisk = 'low' | 'medium' | 'high';
15
+ export interface ClassifiedApproval {
16
+ decision: ApprovalDecision;
17
+ risk: AutoApprovalRisk;
18
+ /** Short stable id for i18n / model feedback. */
19
+ reasonKey: string;
20
+ }
14
21
  /**
15
- * Whole-command danger patterns, checked before anything else. A match keeps
16
- * the interactive prompt regardless of what else the command contains.
22
+ * Whole-command danger patterns, checked before anything else. A match
23
+ * auto-rejects. This is a UX heuristic, not a security boundary: obfuscated
24
+ * or interpreter-wrapped damage still has to be contained by the sandbox.
25
+ *
26
+ * Interpreter `-c`/`-e` is NOT here: the table cannot see the payload, so
27
+ * those shapes ask (AI review) instead of a blanket deny.
17
28
  */
18
29
  export declare const DANGER_PATTERNS: RegExp[];
19
30
  /**
20
31
  * Segment-level allow patterns: low-risk, high-frequency reads, builds, and
21
32
  * tests. A command auto-approves only when EVERY segment matches one of these.
33
+ * `env`/`printenv` stay off this list — process env often holds keys.
22
34
  */
23
35
  export declare const ALLOW_SEGMENT_PATTERNS: RegExp[];
36
+ /**
37
+ * Split a shell command into segments at &&, ||, ; and | boundaries,
38
+ * skipping those operators when they sit inside quotes or a here-doc
39
+ * terminator. Nested quoting is best-effort — the sandbox still owns
40
+ * real damage containment.
41
+ */
42
+ export declare function segments(command: string): string[];
43
+ export declare function touchesSensitivePath(text: string): boolean;
24
44
  /**
25
45
  * Classify one shell command line. Danger anywhere auto-rejects; otherwise
26
46
  * the command auto-approves only when every segment is a recognized low-risk
27
47
  * pattern — unknown shapes ask (and detach to a rejection when unattended).
28
48
  */
29
49
  export declare function classifyCommand(command: string): ApprovalDecision;
50
+ export declare function classifyCommandDetailed(command: string): ClassifiedApproval;
51
+ export interface ClassifyApprovalInput {
52
+ toolName: string;
53
+ command?: string;
54
+ args?: string;
55
+ reason?: string;
56
+ workspaceCwd?: string;
57
+ }
30
58
  /**
31
59
  * Classify one approval request. `command` is the decoded shell command when
32
60
  * the pending call is a shell tool. 'deny' auto-rejects (the model reads the
33
61
  * rejection and adapts); 'ask' falls through to the interactive prompt.
34
62
  */
35
63
  export declare function classifyApproval(toolName: string, command: string | undefined): ApprovalDecision;
64
+ export declare function classifyApprovalDetailed(input: ClassifyApprovalInput): ClassifiedApproval;
36
65
  /**
37
66
  * Decode the shell command of a streamed tool call from its raw JSON args.
38
67
  * Returns undefined for non-shell tools or unparseable args.
39
68
  */
40
69
  export declare function commandFromArgs(toolName: string, args: string): string | undefined;
70
+ /**
71
+ * Pull a shell command out of an approval-request `reason` when the
72
+ * classifier never saw the tool-call JSON (sandbox escalation, missing
73
+ * callId, or a card whose args were not recorded).
74
+ */
75
+ export declare function commandFromApprovalReason(reason: string | undefined): string | undefined;
76
+ /**
77
+ * Resolve the command the classifier should see for one approval request.
78
+ * Prefer the streamed bash/pwsh card, then a command already decoded on the
79
+ * card, then the request's reason text.
80
+ */
81
+ export declare function commandForApprovalRequest(input: {
82
+ toolName: string;
83
+ reason?: string;
84
+ row?: {
85
+ name: string;
86
+ args: string;
87
+ command?: string;
88
+ };
89
+ }): string | undefined;
41
90
  /** Parse the /approval argument into a mode. */
42
91
  export declare function parseAutoApprovalMode(raw: string): AutoApprovalMode | undefined;
92
+ /** True when `/approval <arg>` should print the current mode and counters. */
93
+ export declare function isApprovalStatusArg(raw: string): boolean;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Footer stats, status identity, context-pressure chips, and `/status` report.
3
+ */
4
+ import { type QuotaSnapshot } from './quota.js';
5
+ import type { DisconnectPolicyName } from './transcript-types.js';
6
+ export declare const WAIT_INDICATOR_MS = 8000;
7
+ /** Compact token count, matching the web stats line (517 / 12.2K / 1.2M). */
8
+ export declare function formatTokens(n: number): string;
9
+ /**
10
+ * Prompt occupancy of the next request, from DSH `contextPressure`.
11
+ * Provider-agnostic: uses the routed model's advertised window, not a
12
+ * hardcoded xAI size. Compaction-basic still owns in-turn pressure at 80%.
13
+ */
14
+ export declare const CONTEXT_PRESSURE_WARN_RATIO = 0.8;
15
+ export declare const CONTEXT_PRESSURE_DANGER_RATIO = 0.95;
16
+ /** Idle auto-compact starts here so recovery finishes before the 80% in-turn trigger. */
17
+ export declare const CONTEXT_IDLE_COMPACT_RATIO = 0.72;
18
+ export interface ContextPressureSample {
19
+ usedTokens: number;
20
+ contextWindow: number;
21
+ }
22
+ export interface ContextPressureView {
23
+ usedTokens: number;
24
+ contextWindow: number;
25
+ percent: number;
26
+ level: 'ok' | 'warn' | 'danger';
27
+ }
28
+ /** Prompt-side occupancy of one usage sample: uncached input plus cache traffic. */
29
+ export declare function promptPressureTokens(usage: {
30
+ inputTokens: number;
31
+ cacheReadTokens?: number;
32
+ cacheWriteTokens?: number;
33
+ }): number;
34
+ /** Prefer the next-request projection; fall back to last-request pressure. */
35
+ export declare function contextPressureUsedTokens(pressure: {
36
+ projectedTokens?: number;
37
+ pressureTokens?: number;
38
+ } | undefined): number | undefined;
39
+ export declare function parseContextPressure(value: unknown): ContextPressureSample | undefined;
40
+ export declare function contextPressureView(sample: ContextPressureSample): ContextPressureView;
41
+ /**
42
+ * 8-segment Braille ring. Empty `⣀`; full `⣿`. Width is always 1 cell.
43
+ * Index is `ceil(percent / 12.5)` clamped to 0..8.
44
+ */
45
+ export declare const CONTEXT_RING_EMPTY = "\u28C0";
46
+ export declare const CONTEXT_RING_SEGMENTS: readonly ["⣀", "⠉", "⠋", "⠛", "⠞", "⠟", "⠿", "⡿", "⣿"];
47
+ export declare function formatContextPressureRing(percent: number): string;
48
+ export declare function contextPressureRingColor(level: ContextPressureView['level']): string;
49
+ export declare function formatContextPressureChip(view: ContextPressureView, color?: boolean): string;
50
+ export declare function formatContextPressureStatusLine(view: ContextPressureView | undefined): string;
51
+ export declare function contextPressureAlertText(view: ContextPressureView): string;
52
+ export declare function shouldIdleAutoCompact(view: ContextPressureView | undefined): boolean;
53
+ /** Compact duration, matching the web stats line (45.2s / 2m42s). */
54
+ export declare function formatDuration(ms: number): string;
55
+ export declare function formatTokensPerSecond(tokensPerSecond: number): string;
56
+ export declare function providerShortCode(provider: string): string;
57
+ export interface FooterStatsInput {
58
+ turns: number;
59
+ steps: number;
60
+ llmMs: number;
61
+ toolMs: number;
62
+ ttftMs: number;
63
+ ttftSteps: number;
64
+ decodeMs: number;
65
+ decodeTokens: number;
66
+ inputTokens: number;
67
+ outputTokens: number;
68
+ cacheReadTokens: number;
69
+ cacheWriteTokens: number;
70
+ }
71
+ /** Stats groups in drop order (last is dropped first when the row is too wide). */
72
+ export declare function footerStatsGroups(stats: FooterStatsInput): string[];
73
+ export declare function fitFooterStatsLine(chip: string, groups: readonly string[], width: number): string;
74
+ export type FooterActivityKind = 'plan-review' | 'waiting' | 'compacting' | 'retry' | 'subagents' | 'tools' | 'plan-open' | 'plan-pending' | 'goal' | 'waiting-llm' | 'idle';
75
+ export interface FooterStatusInput {
76
+ running: boolean;
77
+ planReview: boolean;
78
+ waitingQuestion: boolean;
79
+ compacting: boolean;
80
+ retry?: {
81
+ retry: number;
82
+ maxRetries: number;
83
+ };
84
+ subagents: number;
85
+ tools: number;
86
+ planLeftOpen: boolean;
87
+ planPending: boolean;
88
+ planActive: boolean;
89
+ goalPhase?: 'active' | 'paused' | 'blocked';
90
+ idleMs: number;
91
+ model: string;
92
+ effort?: string;
93
+ preset?: string;
94
+ provider: string;
95
+ parentModel: string;
96
+ subModel: string;
97
+ subDiffers: boolean;
98
+ quotaCode?: string;
99
+ quotaPercent?: number;
100
+ contextChip?: string;
101
+ balanceText?: string;
102
+ search?: {
103
+ index: number;
104
+ total: number;
105
+ };
106
+ foldedInput: boolean;
107
+ multiLineInput: boolean;
108
+ queued: number;
109
+ cwdLabel?: string;
110
+ compactView?: boolean;
111
+ }
112
+ export declare function footerActivity(input: FooterStatusInput): {
113
+ kind: FooterActivityKind;
114
+ text: string;
115
+ };
116
+ /** Short remaining-quota bar: 8 pips, filled from the left. */
117
+ export declare function formatQuotaBar(remainingPercent: number, width?: number): string;
118
+ export declare function footerIdentityParts(input: FooterStatusInput): string[];
119
+ /** `SuperGrok ███████░ 82%`, or just the bar + percent when `code` is omitted. */
120
+ export declare function formatFooterQuota(percent: number, code?: string): string;
121
+ /**
122
+ * Drop the Go / SuperGrok plan name from a quota identity part, keeping the
123
+ * remaining-percent bar. Returns true when a part was rewritten.
124
+ */
125
+ export declare function dropFooterQuotaPlanName(parts: string[]): boolean;
126
+ export declare function fitFooterStatusLine(activity: string, identity: readonly string[], width: number): string;
127
+ export interface StatusReportInput {
128
+ sessionId: string;
129
+ pluginVersion: string;
130
+ provider: string;
131
+ model: string;
132
+ effort?: string;
133
+ agentStatus: string;
134
+ preset: string;
135
+ activeSubagents: number;
136
+ plan: 'off' | 'pending' | 'on';
137
+ paint: string;
138
+ disconnect?: DisconnectPolicyName;
139
+ waitingQuestions: number;
140
+ quota?: QuotaSnapshot;
141
+ context?: ContextPressureView;
142
+ parentModel?: string;
143
+ subProvider?: string;
144
+ subModel: string;
145
+ cwd?: string;
146
+ }
147
+ /** Lines printed by `/status` — SSH first-boot diagnostics, no extra command. */
148
+ export declare function formatStatusReport(input: StatusReportInput): string[];
149
+ /** Human-facing kind for a live LLM route. */
150
+ export declare function describeProviderRoute(provider: string): {
151
+ kind: string;
152
+ short: string;
153
+ };
154
+ /** Routes that authenticate without a harness API-key credential. */
155
+ export declare function providerUsesLocalOAuth(provider: string): boolean;
@@ -15,11 +15,13 @@ export declare const UI_LOCALE_SCHEMA: z<Schemastery.ObjectS<{
15
15
  skipUpdate: z<string, string>;
16
16
  view: z<string, string>;
17
17
  disconnect: z<string, string>;
18
+ autoApproval: z<string, string>;
18
19
  }>, Schemastery.ObjectT<{
19
20
  language: z<string, string>;
20
21
  skipUpdate: z<string, string>;
21
22
  view: z<string, string>;
22
23
  disconnect: z<string, string>;
24
+ autoApproval: z<string, string>;
23
25
  }>>;
24
26
  export declare function localeFromTag(tag: string): Locale | undefined;
25
27
  /** Pick zh/en from env, optionally after a saved settings value. */
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Best-effort JSON argument parsing for tool cards and plan payloads.
3
+ */
4
+ export declare function parseJsonArgs(args: string): Record<string, unknown> | null;
5
+ export declare function firstString(record: Record<string, unknown>, keys: readonly string[]): string;
6
+ /** A short scalar rendering of one argument value, or null for objects/arrays. */
7
+ export declare function scalarText(value: unknown): string | null;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Incremental ANSI paint, SSH cadence, hangup signals, and sliding windows.
3
+ *
4
+ * Kept off `tui.ts` so the launch picker can dirty-paint without loading
5
+ * the agent-backed SshTui class.
6
+ */
7
+ export declare const DSR_PROBE_TIMEOUT_MS = 800;
8
+ /** Give a running turn this long to settle after cancel before we flush anyway. */
9
+ export declare const HANGUP_CANCEL_TIMEOUT_MS = 10000;
10
+ export type PaintLinkKind = 'local' | 'ssh';
11
+ /** In-app dialog / slash-suggestion page size. Launch picker uses 9 of its own. */
12
+ export declare const PICKER_WINDOW = 12;
13
+ /**
14
+ * Explicit env/config always wins. Otherwise local TTYs stay snappy and SSH
15
+ * sessions pick a tier from a measured round-trip (CSI 6n), falling back to
16
+ * 160 ms when the probe is missing.
17
+ */
18
+ export declare function resolvePaintIntervalMs(configured?: number, env?: NodeJS.ProcessEnv, options?: {
19
+ ssh?: boolean;
20
+ rttMs?: number;
21
+ }): number;
22
+ /** True when this process is attached to an SSH session (jump host / proxy). */
23
+ export declare function detectSshSession(env?: NodeJS.ProcessEnv): boolean;
24
+ /** Node errno on a write/close that means the TTY is gone (SSH drop, HUP). */
25
+ export declare function isHangupErrno(error: unknown): boolean;
26
+ /**
27
+ * Replace launcher SIGTERM/SIGINT/SIGHUP handlers with `handler`. SSH drop
28
+ * otherwise lets `dsh` dispose the whole tree before this plugin can detach.
29
+ */
30
+ export declare function captureHangupSignals(handler: () => void): void;
31
+ export declare function releaseHangupSignals(handler: () => void): void;
32
+ /** After detach, extra HUP/TERM from sshd must not kill the leftover Host. */
33
+ export declare function ignoreFurtherHangupSignals(): void;
34
+ /**
35
+ * Wait until `isIdle` is true or `timeoutMs` elapses. Used after cancel so a
36
+ * hangup can flush a settled session log instead of tearing a live write.
37
+ */
38
+ export declare function waitUntilIdleOrTimeout(isIdle: () => boolean, timeoutMs: number, now?: () => number, wait?: (ms: number) => Promise<void>): Promise<'idle' | 'timeout'>;
39
+ /** Map a CSI-6n round-trip to a paint cadence. Unknown RTT uses the SSH default. */
40
+ export declare function paintIntervalForRtt(rttMs: number | undefined): number;
41
+ export declare function paintLinkLabel(kind: PaintLinkKind, intervalMs: number, probed: boolean): string;
42
+ export type LinkQuality = 'local' | 'good' | 'ok' | 'slow' | 'poor' | 'unknown';
43
+ /** Signal-bar quality from a measured SSH round-trip, or local TTY. */
44
+ export declare function linkQualityOf(kind: PaintLinkKind, rttMs: number | undefined): LinkQuality;
45
+ /** How many filled signal pips: 4 local/fast, 3 ok, 2 slow, 1 poor, 0 unknown. */
46
+ export declare function linkSignalPips(quality: LinkQuality): number;
47
+ /** Compact footer chip: `SSH ●●●○ 90ms` — 1 pip red, 2 yellow, 3+ green. */
48
+ export declare function formatLinkQualityChip(kind: PaintLinkKind, intervalMs: number, rttMs: number | undefined, probed: boolean, color?: boolean): string;
49
+ /** One incremental paint as a single stdout write (one SSH packet when corked). */
50
+ export declare function composePaintOutput(options: {
51
+ width: number;
52
+ height: number;
53
+ paintRows: readonly string[];
54
+ previousRows: readonly string[];
55
+ sizeChanged: boolean;
56
+ chromeChanged: boolean;
57
+ chromeStart: number;
58
+ previousChromeStart?: number;
59
+ cursorRow: number;
60
+ cursorColumn: number;
61
+ /** Keep the cursor hidden (session picker). Default shows it on the input. */
62
+ hideCursor?: boolean;
63
+ }): string;
64
+ /** Whether `text` could still grow into a recognized escape sequence. */
65
+ export declare function isEscapePrefix(text: string): boolean;
66
+ /** Parse a Device Status Report cursor reply (`CSI row;col R`). */
67
+ export declare function parseCursorPositionReply(text: string): {
68
+ row: number;
69
+ column: number;
70
+ } | undefined;
71
+ /**
72
+ * Round-trip to the attached terminal via CSI 6n. Returns undefined when the
73
+ * reply never arrives (dumb pipe, blocked DSR). Does not interpret the
74
+ * coordinates — only the elapsed milliseconds matter.
75
+ */
76
+ export declare function probeTerminalRttMs(stdin?: NodeJS.ReadStream, stdout?: NodeJS.WriteStream, timeoutMs?: number): Promise<number | undefined>;
77
+ /** Sliding window of `windowSize` items that keeps `cursor` visible. */
78
+ export declare function pickerWindowStart(cursor: number, total: number, windowSize?: number): number;
@@ -1,9 +1,14 @@
1
1
  /**
2
- * Startup history-session picker: a small raw-mode selector shown BEFORE the
3
- * main TUI mounts, so launching without an explicit session id lands on a
4
- * choice instead of a fresh main screen.
2
+ * Startup history-session picker: a raw-mode selector shown BEFORE the main
3
+ * TUI mounts, so launching without an explicit session id lands on a choice
4
+ * instead of a fresh main screen.
5
+ *
6
+ * The list itself is not capped. The visible page is always nine rows so
7
+ * digits 1-9 map onto every on-screen item. Arrow keys move a highlight,
8
+ * typing filters by title / id / cwd, and Enter confirms the focused row.
5
9
  */
6
10
  import type { Context } from '@deepseek-ai/cordis';
11
+ import { type ResumableSession } from './session-list.js';
7
12
  /** What the launch picker decided. */
8
13
  export type SessionPickerResult = {
9
14
  kind: 'resume';
@@ -16,10 +21,97 @@ export type SessionPickerResult = {
16
21
  kind: 'new';
17
22
  } | null;
18
23
  /**
19
- * Show the picker and wait for one digit, Enter (new session), or a lone
20
- * Esc/Ctrl+C (cancel). Incomplete CSI/SS3 sequences are buffered briefly so
21
- * arrow keys are ignored instead of cancelling. Restores the terminal before
22
- * resolving; an AbortSignal cancels and restores as well.
24
+ * Visible rows in the sliding window. Locked to nine so every on-screen
25
+ * session has a 1-9 shortcut; remaining history is reached with ↑/↓ / filter.
26
+ */
27
+ export declare const SESSION_PICKER_WINDOW = 9;
28
+ /** Digit shortcuts into the current window when the filter is empty. */
29
+ export declare const PICKER_QUICK_KEYS = "123456789";
30
+ /** Mutable picker UI state. */
31
+ export interface SessionPickerState {
32
+ sessions: ResumableSession[];
33
+ query: string;
34
+ cursor: number;
35
+ /**
36
+ * When true, digits type into the filter instead of acting as 1-9 / 0
37
+ * shortcuts. Set automatically by the first letter, or by `/` / Ctrl+F.
38
+ */
39
+ filterActive: boolean;
40
+ }
41
+ /** One key / control action against {@link SessionPickerState}. */
42
+ export type SessionPickerAction = {
43
+ type: 'move';
44
+ delta: number;
45
+ } | {
46
+ type: 'page';
47
+ delta: number;
48
+ } | {
49
+ type: 'home';
50
+ } | {
51
+ type: 'end';
52
+ } | {
53
+ type: 'type';
54
+ text: string;
55
+ } | {
56
+ type: 'backspace';
57
+ } | {
58
+ type: 'clearQuery';
59
+ } | {
60
+ type: 'startFilter';
61
+ } | {
62
+ type: 'quick';
63
+ key: string;
64
+ } | {
65
+ type: 'submit';
66
+ } | {
67
+ type: 'new';
68
+ } | {
69
+ type: 'escape';
70
+ } | {
71
+ type: 'cancel';
72
+ };
73
+ /** Result of applying one action: keep going, or the picker is done. */
74
+ export type SessionPickerStep = {
75
+ kind: 'continue';
76
+ state: SessionPickerState;
77
+ } | {
78
+ kind: 'done';
79
+ result: SessionPickerResult;
80
+ };
81
+ /** Haystack used by the filter: title, id, cwd, and a live pid when attached. */
82
+ export declare function sessionSearchHaystack(session: ResumableSession): string;
83
+ /** Whether one session matches a whitespace-separated query (every token). */
84
+ export declare function sessionMatchesQuery(session: ResumableSession, query: string): boolean;
85
+ /** Sessions still visible under the current filter, in list order. */
86
+ export declare function filterResumableSessions(sessions: readonly ResumableSession[], query: string): ResumableSession[];
87
+ /** Keep `cursor` inside `[0, total)`. Empty lists pin to 0. */
88
+ export declare function clampPickerCursor(cursor: number, total: number): number;
89
+ /**
90
+ * How many session rows to show. Prefer a full page of nine so 1-9 always
91
+ * map onto every on-screen item. Shrink only when the tty cannot fit that
92
+ * page (title + filter + nine items + hints).
93
+ */
94
+ export declare function pickerCapacity(rows: number): number;
95
+ /** Whether two picker states would paint the same frame. */
96
+ export declare function pickerStateUnchanged(previous: SessionPickerState, next: SessionPickerState): boolean;
97
+ /**
98
+ * Apply one picker action. Pure: the TTY layer maps keystrokes onto this, and
99
+ * tests drive it without stdin.
100
+ */
101
+ export declare function stepPicker(state: SessionPickerState, action: SessionPickerAction, windowSize?: number): SessionPickerStep;
102
+ /** Map one decoded stdin chunk (or a complete CSI sequence) to actions. */
103
+ export declare function actionsForInput(text: string, state?: Pick<SessionPickerState, 'query' | 'filterActive'>): SessionPickerAction[];
104
+ /** Split a decoded stdin chunk into CSI/SS3 sequences and individual characters. */
105
+ export declare function splitPickerInput(text: string): string[];
106
+ /**
107
+ * Feed a decoded stdin chunk through the picker. CSI / SS3 sequences are one
108
+ * unit; ordinary text is applied character by character so a pasted "fix 2"
109
+ * types the digit instead of treating it as a 1-9 shortcut.
110
+ */
111
+ export declare function feedPicker(state: SessionPickerState, text: string, windowSize?: number): SessionPickerStep;
112
+ /**
113
+ * Show the picker and wait for a selection, a new-session request, or cancel.
114
+ * Restores the terminal before resolving; an AbortSignal cancels as well.
23
115
  * @param ctx - boot context supplying sessionPersistence.
24
116
  * @param color - whether to apply ANSI colors.
25
117
  * @param signal - optional abort signal (fiber dispose) to cancel the picker.
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Plan dock, todo lists, /find, prompt-injection cards, and compact errors.
3
+ */
4
+ import type { DisplayKind, PlanTodoItem, Row, SubagentLogEntry } from './transcript-types.js';
5
+ export declare const MAX_SUBAGENT_LOGS = 80;
6
+ export declare const TODO_STATUS_MARK: Record<PlanTodoItem['status'], string>;
7
+ /** True while a plan still belongs in the dock (latest incomplete work). */
8
+ export declare function planIsLive(plan: {
9
+ active: boolean;
10
+ pending: boolean;
11
+ todos: readonly PlanTodoItem[];
12
+ planMarkdown?: string;
13
+ archived?: boolean;
14
+ }): boolean;
15
+ /** Open todos left behind when a turn ends without a completing todo_write. */
16
+ export declare function planTurnLeftOpen(plan: {
17
+ todos: readonly PlanTodoItem[];
18
+ }): boolean;
19
+ /** Mark leftover in-progress/pending todos as display-stale after turn/end. */
20
+ export declare function applyTurnEndToPlan<T extends {
21
+ todos: PlanTodoItem[];
22
+ turnLeftOpen?: boolean;
23
+ }>(plan: T): T;
24
+ /** Follow-up that asks the model to close leftover todos. One per open list. */
25
+ export declare function planCloseNudgeText(plan: {
26
+ todos: readonly PlanTodoItem[];
27
+ }): string;
28
+ export type CardCategory = 'thinking' | 'plan' | 'subagent' | 'reply' | 'tool' | 'question' | 'goal' | 'prompt';
29
+ /** Category for jump / search. Assistant replies are not collapsible cards. */
30
+ export declare function cardCategoryOf(row: {
31
+ kind: string;
32
+ }): CardCategory | undefined;
33
+ export declare function cardCategoryLabel(category: CardCategory): string;
34
+ export declare function parseCardCategoryToken(token: string): CardCategory | undefined;
35
+ /** Split `/find thinking padAnsi` into an optional category and a query. */
36
+ export declare function parseFindQuery(raw: string): {
37
+ category?: CardCategory;
38
+ query: string;
39
+ };
40
+ /** Classify one injected prompt blob into display sources. */
41
+ export declare function promptInjectionSources(text: string, plugin?: string): string[];
42
+ export declare function promptInjectionTitle(sources: readonly string[]): string;
43
+ export declare function isPromptInjectionMessage(sourceKind: string, text: string, plugin?: string): boolean;
44
+ /** Official `/compact` idle-only failures, mapped to a local sentence. */
45
+ export declare function formatCompactCommandError(text: string): string;
46
+ export declare function compactionHeaderText(row: {
47
+ status: 'running' | 'ok' | 'error';
48
+ pruneCount: number;
49
+ prunedTokens: number;
50
+ error?: string;
51
+ }): string;
52
+ /** Transcript rows matching a `/find` query, newest last. */
53
+ export declare function matchTranscriptRows(rows: readonly Row[], raw: string): Row[];
54
+ /** One-line note under an expanded plan strip. */
55
+ export declare function planDockNote(plan: {
56
+ active: boolean;
57
+ pending: boolean;
58
+ todos: readonly PlanTodoItem[];
59
+ planMarkdown?: string;
60
+ turnLeftOpen?: boolean;
61
+ }): string;
62
+ /** Compact per-status counts matching the web plan strip. */
63
+ export declare function todoProgressLabel(todos: readonly PlanTodoItem[]): string;
64
+ export declare function todoItemKind(status: PlanTodoItem['status']): DisplayKind;
65
+ export declare function planMarkdownFromArgs(value: unknown): string | undefined;
66
+ /** First markdown heading of an exit_plan_mode plan body. */
67
+ export declare function planTitleFromMarkdown(markdown: string): string | undefined;
68
+ /** Parse a todo_write payload into displayable plan items. */
69
+ export declare function parsePlanTodos(value: unknown): PlanTodoItem[];
70
+ /** Compact todo-list summary: done/total plus the first in-progress task. */
71
+ export declare function todoSummary(value: unknown): string;
72
+ /** Compact ask_user_question summary from tool arguments. */
73
+ export declare function askSummary(value: unknown): string;
74
+ /** One-line subagent card header used while collapsed. */
75
+ export declare function subagentHeaderText(row: Extract<Row, {
76
+ kind: 'subagent';
77
+ }>, now?: number): string;
78
+ export declare function appendSubagentLog(row: Extract<Row, {
79
+ kind: 'subagent';
80
+ }>, entry: SubagentLogEntry): void;