@gajae-code/coding-agent 0.12.7 → 0.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +31 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/session/session-storage.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/src/cli/notify-cli.ts +204 -94
- package/src/commands/notify.ts +19 -8
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-registry.ts +862 -75
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +464 -50
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +97 -35
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +431 -130
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +10 -7
- package/src/session/agent-session.ts +44 -24
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +382 -45
- package/src/session/internal/native-publish-outcome.ts +3 -1
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/session-manager.ts +27 -3
- package/src/session/session-storage.ts +72 -5
- package/src/slash-commands/builtin-registry.ts +82 -4
- package/src/task/index.ts +9 -3
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/telegram-send.ts +4 -2
package/src/main.ts
CHANGED
|
@@ -57,10 +57,11 @@ import {
|
|
|
57
57
|
discoverAuthStorage,
|
|
58
58
|
} from "./sdk";
|
|
59
59
|
import type { AgentSession } from "./session/agent-session";
|
|
60
|
-
|
|
60
|
+
import { SessionMigrationBusyError } from "./session/internal/session-open-errors";
|
|
61
61
|
import {
|
|
62
62
|
type ResumeSessionIdentity,
|
|
63
63
|
resolveResumableSession,
|
|
64
|
+
SessionArtifactCapacityError,
|
|
64
65
|
type SessionDestination,
|
|
65
66
|
type SessionDirectoryMigrationPolicy,
|
|
66
67
|
type SessionInfo,
|
|
@@ -562,6 +563,22 @@ export function resolveManagedAgentDirForScope(_cwd: string): string {
|
|
|
562
563
|
return getAgentDir();
|
|
563
564
|
}
|
|
564
565
|
export const BARE_RESUME_OPEN_ERROR = "Could not open the selected session. Use --resume <id>.";
|
|
566
|
+
const SESSION_ARTIFACT_CAPACITY_RECOVERY_MESSAGE =
|
|
567
|
+
"The selected legacy session's artifacts exceed the supported migration capacity. Archive or remove only that legacy session's artifacts after confirming they are no longer needed, then retry.";
|
|
568
|
+
|
|
569
|
+
function operatorFacingSessionOpenMessage(value: unknown): string | undefined {
|
|
570
|
+
const code =
|
|
571
|
+
value instanceof SessionArtifactCapacityError
|
|
572
|
+
? value.code
|
|
573
|
+
: value instanceof SessionMigrationBusyError
|
|
574
|
+
? value.code
|
|
575
|
+
: typeof value === "string"
|
|
576
|
+
? value
|
|
577
|
+
: undefined;
|
|
578
|
+
if (code === "artifact_capacity_exceeded") return SESSION_ARTIFACT_CAPACITY_RECOVERY_MESSAGE;
|
|
579
|
+
if (code === "migration_busy") return new SessionMigrationBusyError().message;
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
565
582
|
|
|
566
583
|
function isBareResume(parsed: Args): boolean {
|
|
567
584
|
return (
|
|
@@ -1072,6 +1089,7 @@ type RunPrintMode = (session: AgentSession, options: PrintModeOptions) => Promis
|
|
|
1072
1089
|
|
|
1073
1090
|
export interface RunRootCommandDependencies {
|
|
1074
1091
|
createAgentSession?: typeof createAgentSession;
|
|
1092
|
+
createSessionManager?: typeof createSessionManager;
|
|
1075
1093
|
discoverAuthStorage?: typeof discoverAuthStorage;
|
|
1076
1094
|
runAcpMode?: (options?: { agentDir?: string }) => Promise<void>;
|
|
1077
1095
|
settings?: Settings;
|
|
@@ -1208,13 +1226,13 @@ export async function runRootCommand(
|
|
|
1208
1226
|
undefined,
|
|
1209
1227
|
resumeMigrationPolicy,
|
|
1210
1228
|
);
|
|
1211
|
-
} catch {
|
|
1212
|
-
process.stderr.write(`${BARE_RESUME_OPEN_ERROR}\n`);
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
process.stderr.write(`${operatorFacingSessionOpenMessage(error) ?? BARE_RESUME_OPEN_ERROR}\n`);
|
|
1213
1231
|
if (!deps.suppressProcessExit) process.exitCode = 1;
|
|
1214
1232
|
return;
|
|
1215
1233
|
}
|
|
1216
1234
|
if (opened.kind === "error") {
|
|
1217
|
-
process.stderr.write(`${BARE_RESUME_OPEN_ERROR}\n`);
|
|
1235
|
+
process.stderr.write(`${operatorFacingSessionOpenMessage(opened.reason) ?? BARE_RESUME_OPEN_ERROR}\n`);
|
|
1218
1236
|
if (!deps.suppressProcessExit) process.exitCode = 1;
|
|
1219
1237
|
return;
|
|
1220
1238
|
}
|
|
@@ -1381,9 +1399,27 @@ export async function runRootCommand(
|
|
|
1381
1399
|
|
|
1382
1400
|
// Create session manager based on CLI flags. A bare resume was strictly opened
|
|
1383
1401
|
// before startup discovery, so it never reaches create-or-open behavior here.
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1402
|
+
let sessionManager: SessionManager | undefined = bareResumeSessionManager;
|
|
1403
|
+
if (!sessionManager) {
|
|
1404
|
+
try {
|
|
1405
|
+
sessionManager = await logger.time(
|
|
1406
|
+
"createSessionManager",
|
|
1407
|
+
deps.createSessionManager ?? createSessionManager,
|
|
1408
|
+
parsedArgs,
|
|
1409
|
+
cwd,
|
|
1410
|
+
settingsInstance,
|
|
1411
|
+
);
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
const message = operatorFacingSessionOpenMessage(error);
|
|
1414
|
+
if (!message) throw error;
|
|
1415
|
+
process.stderr.write(`${message}\n`);
|
|
1416
|
+
if (!deps.suppressProcessExit) process.exitCode = 1;
|
|
1417
|
+
authStorage.close();
|
|
1418
|
+
stopThemeWatcher();
|
|
1419
|
+
await postmortem.cleanup();
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1387
1423
|
|
|
1388
1424
|
// Restore the resumed session's working directory so the HUD branch, the
|
|
1389
1425
|
// project path, and the agent's tools all match where the session was
|
package/src/modes/DESIGN.md
CHANGED
|
@@ -187,21 +187,21 @@ combine, or substitute them.
|
|
|
187
187
|
|
|
188
188
|
| State ID | Required condition represented |
|
|
189
189
|
| --- | --- |
|
|
190
|
-
| `home-unconfigured` | No
|
|
191
|
-
| `home-configured-inactive` |
|
|
190
|
+
| `home-unconfigured` | No complete notification provider. |
|
|
191
|
+
| `home-configured-inactive` | Provider configuration and desired intent exist; current session is inactive. |
|
|
192
192
|
| `home-runtime-active` | Current session endpoint is active. |
|
|
193
193
|
| `home-local-off` | Current session is explicitly locally disabled. |
|
|
194
|
-
| `home-env-off` |
|
|
195
|
-
| `home-env-on` | Explicit environment opt-in enables
|
|
196
|
-
| `home-discord-only` |
|
|
197
|
-
| `home-slack-only` |
|
|
198
|
-
| `setup-provider` |
|
|
194
|
+
| `home-env-off` | Process-level environment hard-off suppresses the surface/runtime. |
|
|
195
|
+
| `home-env-on` | Explicit environment opt-in enables automatic current-session admission. |
|
|
196
|
+
| `home-discord-only` | Discord is complete, desired on, and effective without Telegram. |
|
|
197
|
+
| `home-slack-only` | Slack is complete, desired on, and effective without Telegram. |
|
|
198
|
+
| `setup-provider` | First-class Telegram, Discord, and Slack provider choice is focused. |
|
|
199
199
|
| `setup-chat-entry` | Telegram chat ID field is focused. |
|
|
200
200
|
| `setup-token-entry` | Masked Telegram token field is focused. |
|
|
201
201
|
| `setup-validating` | Token/destination validation is pending. |
|
|
202
202
|
| `setup-threaded-warning` | Threaded mode compatibility warning is visible. |
|
|
203
203
|
| `setup-pairing` | Cancellable private-chat pairing/discovery is pending. |
|
|
204
|
-
| `setup-review` | Sanitized
|
|
204
|
+
| `setup-review` | Sanitized provider, secret action, desired intent, and destination await explicit save. |
|
|
205
205
|
| `saving` | Durable atomic save is in progress and guarded. |
|
|
206
206
|
| `health-probing` | Non-cancellable health probe is in progress and guarded. |
|
|
207
207
|
| `health-ok` | Health report is successful. |
|
|
@@ -211,12 +211,12 @@ combine, or substitute them.
|
|
|
211
211
|
| `recovering` | Recovery action is in progress and guarded. |
|
|
212
212
|
| `reconnecting` | Reconnect action is in progress and guarded. |
|
|
213
213
|
| `navigation-locked` | A guarded operation explains why Tab/Escape cannot leave. |
|
|
214
|
-
| `confirmation-remove` | Adapter-local Telegram removal awaits confirmation. |
|
|
215
|
-
| `confirmation-disable` | Global disable awaits confirmation. |
|
|
214
|
+
| `confirmation-remove` | Adapter-local Telegram removal awaits confirmation; sibling/global state is preserved. |
|
|
215
|
+
| `confirmation-disable` | Global disable awaits confirmation and preserves provider credentials/intent. |
|
|
216
216
|
| `success` | A completed operation has concise success copy. |
|
|
217
217
|
| `preferences` | Notification preferences are visible and editable. |
|
|
218
218
|
| `error` | A sanitized operation failure has recovery guidance. |
|
|
219
|
-
| `foreign-blocked` |
|
|
219
|
+
| `foreign-blocked` | Telegram is excluded from the shared endpoint while effective chat siblings use isolated discovery. |
|
|
220
220
|
| `blocked-restore-retain` | A blocked post-save identity race requires Restore or Retain before navigation. |
|
|
221
221
|
| `cancellation` | A cancellable setup/pairing action was cancelled and restored. |
|
|
222
222
|
| `narrow-cjk` | Narrow localized CJK content exercises semantic line wrapping. |
|
|
@@ -47,6 +47,21 @@ export interface HookSelectorOptions {
|
|
|
47
47
|
*/
|
|
48
48
|
wrapFocused?: boolean;
|
|
49
49
|
scrollTitleRows?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Maximum visible rows for the inline editor in bounded scroll-title selectors.
|
|
52
|
+
* The editor keeps the full draft and scrolls within this height.
|
|
53
|
+
*/
|
|
54
|
+
inlineEditorMaxHeight?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Maximum visible autocomplete rows for the inline editor in a bounded selector.
|
|
57
|
+
* The Editor retains its normal minimum and scroll indicator behavior.
|
|
58
|
+
*/
|
|
59
|
+
inlineAutocompleteMaxVisible?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Compact bounded-selector chrome while the inline editor is active.
|
|
62
|
+
* Used only when the terminal cannot fit the normal editor and dropdown rows.
|
|
63
|
+
*/
|
|
64
|
+
compactInlineInput?: boolean;
|
|
50
65
|
/**
|
|
51
66
|
* Inline free-text entry for the option with this label (e.g. the ask
|
|
52
67
|
* tool's "Other (type your own)"). Selecting it keeps the title and option
|
|
@@ -103,6 +118,7 @@ class ScrollableTitle extends Container {
|
|
|
103
118
|
#maxRows: number;
|
|
104
119
|
#scrollOffset = 0;
|
|
105
120
|
#lastMaxScrollOffset = 0;
|
|
121
|
+
#lastRenderWidth?: number;
|
|
106
122
|
|
|
107
123
|
constructor(title: string, maxRows: number) {
|
|
108
124
|
super();
|
|
@@ -110,9 +126,15 @@ class ScrollableTitle extends Container {
|
|
|
110
126
|
this.#markdown = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
|
|
111
127
|
}
|
|
112
128
|
|
|
113
|
-
setText(text: string): void {
|
|
129
|
+
setText(text: string, resetScroll = true): void {
|
|
114
130
|
this.#markdown.setText(text);
|
|
115
|
-
this.#scrollOffset = 0;
|
|
131
|
+
if (resetScroll) this.#scrollOffset = 0;
|
|
132
|
+
this.invalidate();
|
|
133
|
+
}
|
|
134
|
+
setMaxRows(maxRows: number): void {
|
|
135
|
+
const wasAtBottom = this.#lastMaxScrollOffset > 0 && this.#scrollOffset >= this.#lastMaxScrollOffset;
|
|
136
|
+
this.#maxRows = Math.max(1, Math.floor(maxRows));
|
|
137
|
+
if (wasAtBottom) this.#scrollOffset = Number.MAX_SAFE_INTEGER;
|
|
116
138
|
this.invalidate();
|
|
117
139
|
}
|
|
118
140
|
|
|
@@ -125,7 +147,30 @@ class ScrollableTitle extends Container {
|
|
|
125
147
|
}
|
|
126
148
|
|
|
127
149
|
render(width: number): string[] {
|
|
128
|
-
const
|
|
150
|
+
const wasAtBottom = this.#lastMaxScrollOffset > 0 && this.#scrollOffset >= this.#lastMaxScrollOffset;
|
|
151
|
+
let lines: string[];
|
|
152
|
+
if (this.#lastRenderWidth !== undefined && this.#lastRenderWidth !== width) {
|
|
153
|
+
const previous = this.#markdown.renderWithViewportAnchorSource(this.#lastRenderWidth, { id: "title" });
|
|
154
|
+
const previousOffset = Math.min(this.#scrollOffset, previous.anchors.length - 1);
|
|
155
|
+
const previousAnchor =
|
|
156
|
+
previous.anchors[previousOffset] ??
|
|
157
|
+
previous.anchors.slice(previousOffset + 1).find(anchor => anchor !== null) ??
|
|
158
|
+
previous.anchors.slice(0, previousOffset).findLast(anchor => anchor !== null);
|
|
159
|
+
const reflowed = this.#markdown.renderWithViewportAnchorSource(width, { id: "title" });
|
|
160
|
+
lines = reflowed.lines;
|
|
161
|
+
if (previousAnchor) {
|
|
162
|
+
const anchoredLine = reflowed.anchors.findIndex(
|
|
163
|
+
anchor =>
|
|
164
|
+
anchor !== null &&
|
|
165
|
+
anchor.graphemeStart <= previousAnchor.graphemeStart &&
|
|
166
|
+
previousAnchor.graphemeStart < anchor.graphemeEnd,
|
|
167
|
+
);
|
|
168
|
+
if (anchoredLine >= 0) this.#scrollOffset = anchoredLine;
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
lines = this.#markdown.render(width);
|
|
172
|
+
}
|
|
173
|
+
this.#lastRenderWidth = width;
|
|
129
174
|
if (lines.length <= this.#maxRows) {
|
|
130
175
|
this.#lastMaxScrollOffset = 0;
|
|
131
176
|
this.#scrollOffset = 0;
|
|
@@ -137,22 +182,17 @@ class ScrollableTitle extends Container {
|
|
|
137
182
|
this.#lastMaxScrollOffset = maxScrollOffset;
|
|
138
183
|
this.#scrollOffset = Math.max(0, Math.min(this.#scrollOffset, maxScrollOffset));
|
|
139
184
|
|
|
140
|
-
|
|
141
|
-
const indicator =
|
|
142
|
-
this.#scrollOffset === 0
|
|
143
|
-
? theme.fg("dim", " PgDn↓")
|
|
144
|
-
: this.#scrollOffset >= maxScrollOffset
|
|
145
|
-
? theme.fg("dim", " PgUp↑")
|
|
146
|
-
: theme.fg("dim", " PgUp/PgDn↕");
|
|
147
|
-
const lastIndex = visibleLines.length - 1;
|
|
148
|
-
const availableWidth = Math.max(1, width - visibleWidth(indicator));
|
|
149
|
-
const fittedLine = truncateToWidth(visibleLines[lastIndex] ?? "", availableWidth);
|
|
150
|
-
visibleLines[lastIndex] = `${fittedLine}${indicator}`;
|
|
151
|
-
return visibleLines;
|
|
185
|
+
return lines.slice(this.#scrollOffset, this.#scrollOffset + this.#maxRows);
|
|
152
186
|
}
|
|
153
187
|
|
|
188
|
+
if (wasAtBottom) {
|
|
189
|
+
// The previous render may have reached the bottom after converging
|
|
190
|
+
// away the bottom indicator. Re-anchor before recomputing indicators
|
|
191
|
+
// so a timer repaint cannot resurrect a stale `▼ more` row.
|
|
192
|
+
this.#scrollOffset = Math.max(0, lines.length - (this.#maxRows - 1));
|
|
193
|
+
}
|
|
154
194
|
let showTopIndicator = this.#scrollOffset > 0;
|
|
155
|
-
let showBottomIndicator =
|
|
195
|
+
let showBottomIndicator = !wasAtBottom;
|
|
156
196
|
let contentRows = 1;
|
|
157
197
|
let maxScrollOffset = 0;
|
|
158
198
|
|
|
@@ -234,12 +274,14 @@ class FocusAwareList extends Container {
|
|
|
234
274
|
);
|
|
235
275
|
const focusedWrappedSegments = wrapTextWithAnsi(focusedLabel, availableLabelWidth);
|
|
236
276
|
|
|
237
|
-
// Reserve one row
|
|
238
|
-
//
|
|
239
|
-
// wrap-focused behavior and spend the full viewport on label context.
|
|
277
|
+
// Reserve one row when a non-full focused block and its sibling window
|
|
278
|
+
// cannot both fit. A full focused block keeps its existing priority.
|
|
240
279
|
const totalOptions = this.#options.length;
|
|
241
280
|
const mustCompactFocused = focusedWrappedSegments.length > this.#maxVisibleRows;
|
|
242
|
-
const
|
|
281
|
+
const mustMarkClippedSiblings =
|
|
282
|
+
focusedWrappedSegments.length < this.#maxVisibleRows &&
|
|
283
|
+
focusedWrappedSegments.length + totalOptions - 1 > this.#maxVisibleRows;
|
|
284
|
+
const positionMarkerSlot = (mustCompactFocused || mustMarkClippedSiblings) && totalOptions > 1 ? 1 : 0;
|
|
243
285
|
const focusedBudget = Math.max(1, this.#maxVisibleRows - positionMarkerSlot);
|
|
244
286
|
const focusedSegments = this.#capFocusedSegments(focusedWrappedSegments, focusedBudget, availableLabelWidth);
|
|
245
287
|
const focusedRows = Math.max(1, focusedSegments.length);
|
|
@@ -296,7 +338,7 @@ class FocusAwareList extends Container {
|
|
|
296
338
|
if (rows.length <= budget) return rows;
|
|
297
339
|
|
|
298
340
|
if (budget === 1) {
|
|
299
|
-
return [truncateToWidth(
|
|
341
|
+
return [truncateToWidth(rows[0] ?? "", availableLabelWidth)];
|
|
300
342
|
}
|
|
301
343
|
|
|
302
344
|
if (budget === 2) {
|
|
@@ -347,6 +389,7 @@ export class HookSelectorComponent extends Container {
|
|
|
347
389
|
#wrapFocused: boolean;
|
|
348
390
|
#outline: boolean;
|
|
349
391
|
#scrollTitleRows: number | undefined;
|
|
392
|
+
#inlineEditorMaxHeight: number | undefined;
|
|
350
393
|
#customInput: { optionLabel: string; onSubmit: (text: string) => void } | undefined;
|
|
351
394
|
#clarificationInput: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
352
395
|
#activeInput: { onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
@@ -357,6 +400,10 @@ export class HookSelectorComponent extends Container {
|
|
|
357
400
|
#tui: TUI | undefined;
|
|
358
401
|
#autocompleteProvider: AutocompleteProvider | undefined;
|
|
359
402
|
#acceleratorMap: Readonly<Record<string, string>> | undefined;
|
|
403
|
+
#inlineAutocompleteMaxVisible: number | undefined;
|
|
404
|
+
#compactInlineInput: boolean;
|
|
405
|
+
#titleSpacer: Spacer;
|
|
406
|
+
#inputSpacer: Spacer;
|
|
360
407
|
constructor(
|
|
361
408
|
title: string,
|
|
362
409
|
options: string[],
|
|
@@ -368,7 +415,6 @@ export class HookSelectorComponent extends Container {
|
|
|
368
415
|
|
|
369
416
|
this.#options = options;
|
|
370
417
|
this.#selectedIndex = Math.min(opts?.initialIndex ?? 0, options.length - 1);
|
|
371
|
-
this.#maxVisible = Math.max(3, opts?.maxVisible ?? 12);
|
|
372
418
|
this.#onSelectCallback = onSelect;
|
|
373
419
|
this.#onCancelCallback = onCancel;
|
|
374
420
|
this.#baseTitle = title;
|
|
@@ -382,13 +428,21 @@ export class HookSelectorComponent extends Container {
|
|
|
382
428
|
this.#tui = opts?.tui;
|
|
383
429
|
this.#autocompleteProvider = opts?.autocompleteProvider;
|
|
384
430
|
this.#acceleratorMap = opts?.acceleratorMap;
|
|
431
|
+
this.#inlineAutocompleteMaxVisible =
|
|
432
|
+
opts?.inlineAutocompleteMaxVisible === undefined
|
|
433
|
+
? undefined
|
|
434
|
+
: Math.max(3, Math.floor(opts.inlineAutocompleteMaxVisible));
|
|
435
|
+
this.#compactInlineInput = opts?.compactInlineInput === true;
|
|
385
436
|
|
|
386
437
|
this.addChild(new DynamicBorder());
|
|
387
438
|
this.addChild(new Spacer(1));
|
|
388
439
|
|
|
389
440
|
const scrollTitleRows =
|
|
390
441
|
opts?.scrollTitleRows === undefined ? undefined : Math.max(1, Math.floor(opts.scrollTitleRows));
|
|
442
|
+
this.#maxVisible = Math.max(scrollTitleRows === undefined ? 3 : 1, opts?.maxVisible ?? 12);
|
|
391
443
|
this.#scrollTitleRows = scrollTitleRows;
|
|
444
|
+
this.#inlineEditorMaxHeight =
|
|
445
|
+
opts?.inlineEditorMaxHeight === undefined ? undefined : Math.max(1, Math.floor(opts.inlineEditorMaxHeight));
|
|
392
446
|
if (scrollTitleRows === undefined) {
|
|
393
447
|
this.#titleComponent = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
|
|
394
448
|
} else {
|
|
@@ -396,13 +450,20 @@ export class HookSelectorComponent extends Container {
|
|
|
396
450
|
this.#titleComponent = this.#scrollableTitle;
|
|
397
451
|
}
|
|
398
452
|
this.addChild(this.#titleComponent);
|
|
399
|
-
this
|
|
453
|
+
this.#titleSpacer = new Spacer(1);
|
|
454
|
+
this.addChild(this.#titleSpacer);
|
|
400
455
|
|
|
401
456
|
if (opts?.timeout && opts.timeout > 0 && opts.tui) {
|
|
402
457
|
this.#countdown = new CountdownTimer(
|
|
403
458
|
opts.timeout,
|
|
404
459
|
opts.tui,
|
|
405
|
-
s =>
|
|
460
|
+
s => {
|
|
461
|
+
if (this.#scrollableTitle) {
|
|
462
|
+
this.#scrollableTitle.setText(`${this.#baseTitle} (${s}s)`, false);
|
|
463
|
+
} else {
|
|
464
|
+
this.#titleComponent.setText(`${this.#baseTitle} (${s}s)`);
|
|
465
|
+
}
|
|
466
|
+
},
|
|
406
467
|
() => {
|
|
407
468
|
opts?.onTimeout?.();
|
|
408
469
|
// Auto-select current option on timeout (typically the first/recommended option)
|
|
@@ -431,7 +492,8 @@ export class HookSelectorComponent extends Container {
|
|
|
431
492
|
}
|
|
432
493
|
this.#inputArea = new Container();
|
|
433
494
|
this.addChild(this.#inputArea);
|
|
434
|
-
this
|
|
495
|
+
this.#inputSpacer = new Spacer(1);
|
|
496
|
+
this.addChild(this.#inputSpacer);
|
|
435
497
|
this.#baseHelpText = opts?.helpText ?? "up/down navigate enter select esc cancel";
|
|
436
498
|
this.#helpTextComponent = new Text(theme.fg("dim", this.#baseHelpText), 1, 0);
|
|
437
499
|
this.addChild(this.#helpTextComponent);
|
|
@@ -444,6 +506,38 @@ export class HookSelectorComponent extends Container {
|
|
|
444
506
|
hasActiveInlineInput(): boolean {
|
|
445
507
|
return this.#inlineEditor !== undefined;
|
|
446
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Update the bounded selector's title/list row budgets after a terminal resize.
|
|
511
|
+
* The existing title scroll offset, option focus, and inline editor remain intact.
|
|
512
|
+
*/
|
|
513
|
+
setLayoutBudget(
|
|
514
|
+
maxVisible: number,
|
|
515
|
+
scrollTitleRows: number,
|
|
516
|
+
inlineEditorMaxHeight = this.#inlineEditorMaxHeight,
|
|
517
|
+
inlineAutocompleteMaxVisible = this.#inlineAutocompleteMaxVisible,
|
|
518
|
+
compactInlineInput = this.#compactInlineInput,
|
|
519
|
+
): void {
|
|
520
|
+
if (!this.#scrollableTitle) return;
|
|
521
|
+
const titleRows = Math.max(1, Math.floor(scrollTitleRows));
|
|
522
|
+
this.#maxVisible = Math.max(1, Math.floor(maxVisible));
|
|
523
|
+
this.#scrollTitleRows = titleRows;
|
|
524
|
+
this.#inlineEditorMaxHeight =
|
|
525
|
+
inlineEditorMaxHeight === undefined ? undefined : Math.max(1, Math.floor(inlineEditorMaxHeight));
|
|
526
|
+
this.#inlineAutocompleteMaxVisible =
|
|
527
|
+
inlineAutocompleteMaxVisible === undefined ? undefined : Math.max(3, Math.floor(inlineAutocompleteMaxVisible));
|
|
528
|
+
this.#compactInlineInput = compactInlineInput;
|
|
529
|
+
if (this.#inlineEditor) {
|
|
530
|
+
this.#inlineEditor.setMaxHeight(this.#inlineEditorMaxHeight);
|
|
531
|
+
if (this.#inlineAutocompleteMaxVisible !== undefined) {
|
|
532
|
+
this.#inlineEditor.setAutocompleteMaxVisible(this.#inlineAutocompleteMaxVisible);
|
|
533
|
+
}
|
|
534
|
+
this.#inlineEditor.setAutocompleteProvider(this.#compactInlineInput ? undefined : this.#autocompleteProvider);
|
|
535
|
+
}
|
|
536
|
+
this.#setCompactInputSpacing(this.#inlineEditor !== undefined && this.#compactInlineInput);
|
|
537
|
+
this.#scrollableTitle.setMaxRows(titleRows);
|
|
538
|
+
this.#updateList();
|
|
539
|
+
this.invalidate();
|
|
540
|
+
}
|
|
447
541
|
|
|
448
542
|
#updateList(): void {
|
|
449
543
|
if (this.#wrapFocused && this.#focusAwareList) {
|
|
@@ -489,21 +583,24 @@ export class HookSelectorComponent extends Container {
|
|
|
489
583
|
// Reset countdown on any interaction
|
|
490
584
|
this.#countdown?.reset();
|
|
491
585
|
|
|
492
|
-
if (this.#scrollTitleRows !== undefined &&
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
586
|
+
if (this.#scrollTitleRows !== undefined && !this.#inlineEditor?.isAutocompleteOpen()) {
|
|
587
|
+
const titlePageRows = Math.max(1, this.#scrollTitleRows - 2);
|
|
588
|
+
if (matchesKey(keyData, "pageUp")) {
|
|
589
|
+
this.#scrollableTitle?.scrollBy(-titlePageRows);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (matchesKey(keyData, "pageDown")) {
|
|
593
|
+
this.#scrollableTitle?.scrollBy(titlePageRows);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (!this.#inlineEditor && matchesKey(keyData, "ctrl+u")) {
|
|
597
|
+
this.#scrollableTitle?.scrollBy(-titlePageRows);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (!this.#inlineEditor && matchesKey(keyData, "ctrl+d")) {
|
|
601
|
+
this.#scrollableTitle?.scrollBy(titlePageRows);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
507
604
|
}
|
|
508
605
|
if (this.#inlineEditor) {
|
|
509
606
|
this.#handleInputModeKey(keyData, this.#inlineEditor);
|
|
@@ -573,6 +670,10 @@ export class HookSelectorComponent extends Container {
|
|
|
573
670
|
editor.handleInput(keyData);
|
|
574
671
|
}
|
|
575
672
|
|
|
673
|
+
#setCompactInputSpacing(compact: boolean): void {
|
|
674
|
+
this.#titleSpacer.setLines(compact ? 0 : 1);
|
|
675
|
+
this.#inputSpacer.setLines(compact ? 0 : 1);
|
|
676
|
+
}
|
|
576
677
|
#enterInputMode(input: { onSubmit: (text: string) => void; allowEmpty?: boolean }): void {
|
|
577
678
|
if (this.#inlineEditor) return;
|
|
578
679
|
this.#activeInput = input;
|
|
@@ -581,28 +682,37 @@ export class HookSelectorComponent extends Container {
|
|
|
581
682
|
if (this.#countdown) {
|
|
582
683
|
this.#countdown.dispose();
|
|
583
684
|
this.#countdown = undefined;
|
|
584
|
-
|
|
685
|
+
if (this.#scrollableTitle) {
|
|
686
|
+
this.#scrollableTitle.setText(this.#baseTitle, false);
|
|
687
|
+
} else {
|
|
688
|
+
this.#titleComponent.setText(this.#baseTitle);
|
|
689
|
+
}
|
|
585
690
|
}
|
|
586
691
|
const editor = new Editor(getEditorTheme());
|
|
587
692
|
editor.setBorderVisible(false);
|
|
588
693
|
editor.setPromptGutter("> ");
|
|
589
694
|
editor.disableSubmit = true;
|
|
695
|
+
editor.setMaxHeight(this.#inlineEditorMaxHeight);
|
|
696
|
+
if (this.#inlineAutocompleteMaxVisible !== undefined) {
|
|
697
|
+
editor.setAutocompleteMaxVisible(this.#inlineAutocompleteMaxVisible);
|
|
698
|
+
}
|
|
590
699
|
// Mark the inline editor focused only when mirroring the app's hardware-cursor
|
|
591
700
|
// mode, so it emits CURSOR_MARKER at the input caret for IME preedit anchoring
|
|
592
701
|
// without changing legacy non-hardware-cursor layout.
|
|
593
702
|
const useTerminalCursor = this.#tui?.getShowHardwareCursor() ?? false;
|
|
594
703
|
editor.focused = useTerminalCursor;
|
|
595
704
|
editor.setUseTerminalCursor(useTerminalCursor);
|
|
596
|
-
if (this.#autocompleteProvider) {
|
|
705
|
+
if (this.#autocompleteProvider && !this.#compactInlineInput) {
|
|
597
706
|
editor.setAutocompleteProvider(this.#autocompleteProvider);
|
|
598
707
|
}
|
|
599
708
|
this.#inlineEditor = editor;
|
|
600
|
-
this.#
|
|
709
|
+
this.#setCompactInputSpacing(this.#compactInlineInput);
|
|
710
|
+
if (!this.#compactInlineInput) this.#inputArea.addChild(new Spacer(1));
|
|
601
711
|
this.#inputArea.addChild(editor);
|
|
602
712
|
const helpText =
|
|
603
713
|
this.#scrollTitleRows === undefined
|
|
604
714
|
? "enter submit esc back to options ctrl+g external editor"
|
|
605
|
-
: "enter submit esc back to options PgUp/PgDn: question · Wheel: transcript";
|
|
715
|
+
: "enter submit esc back to options ctrl+g external editor PgUp/PgDn: question · Wheel: transcript";
|
|
606
716
|
this.#helpTextComponent.setText(theme.fg("dim", helpText));
|
|
607
717
|
this.invalidate();
|
|
608
718
|
}
|
|
@@ -612,6 +722,7 @@ export class HookSelectorComponent extends Container {
|
|
|
612
722
|
this.#inlineEditor = undefined;
|
|
613
723
|
this.#activeInput = undefined;
|
|
614
724
|
this.#inputArea.clear();
|
|
725
|
+
this.#setCompactInputSpacing(false);
|
|
615
726
|
this.#helpTextComponent.setText(theme.fg("dim", this.#baseHelpText));
|
|
616
727
|
this.invalidate();
|
|
617
728
|
}
|