@hydra-acp/cli 0.1.35 → 0.1.37

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 (2) hide show
  1. package/dist/cli.js +2449 -2252
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4980,1315 +4980,849 @@ var init_update_check = __esm({
4980
4980
  }
4981
4981
  });
4982
4982
 
4983
- // src/tui/picker.ts
4984
- async function pickSession(term, opts) {
4985
- process.stdout.write("\x1B[<u");
4986
- process.stdout.write("\x1B[?2004l");
4987
- process.stdout.write("\x1B[>4;0m");
4988
- process.stdout.write("\x1B[>5;0m");
4989
- process.stdout.write("\x1B[?1000l");
4990
- process.stdout.write("\x1B[?1002l");
4991
- process.stdout.write("\x1B[?1006l");
4992
- process.stdout.write("\x1B[?1l");
4993
- process.stdout.write("\x1B>");
4994
- if (opts.sessions.length === 0) {
4995
- return { kind: "new" };
4996
- }
4997
- const sortSessions = (sessions) => {
4998
- const score = (s) => {
4999
- if (s.status !== "live") {
5000
- return 0;
4983
+ // src/tui/input.ts
4984
+ var InputDispatcher;
4985
+ var init_input = __esm({
4986
+ "src/tui/input.ts"() {
4987
+ "use strict";
4988
+ InputDispatcher = class {
4989
+ buffer = [""];
4990
+ row = 0;
4991
+ col = 0;
4992
+ planMode = false;
4993
+ historyIndex = -1;
4994
+ // Queue editing: when the user walks Up past row 0 with queued prompts
4995
+ // present, the most-recently-queued item lands in the buffer and
4996
+ // queueIndex tracks which slot of `queue` is being edited. Enter submits
4997
+ // the edit (queue-edit) or, on an empty buffer, drops the slot
4998
+ // (queue-remove). -1 means not editing a queue slot.
4999
+ queueIndex = -1;
5000
+ savedDraft = null;
5001
+ history = [];
5002
+ // Active reverse-incremental search over `history`. Set when ^r is
5003
+ // pressed; cleared when the user accepts (Enter / typing / arrows)
5004
+ // or cancels (ESC). `query` is the lowercased substring matched
5005
+ // against history entries; `matchIndices` are history indices in
5006
+ // newest→oldest order; `cursor` is the current index into that list.
5007
+ // `savedDraft` snapshots the buffer/cursor at the moment search
5008
+ // began so ESC can restore it.
5009
+ historySearch = null;
5010
+ // Waiting queue snapshot (excludes the in-flight head). Newest item lives
5011
+ // at the end so Up walks the array right-to-left.
5012
+ queue = [];
5013
+ turnRunning = false;
5014
+ // Single-slot kill ring. The most recent killed text (^U, ^K, ^W) lands
5015
+ // here so ^Y can yank it back. Standard readline keeps a stack; we
5016
+ // only keep one slot because that's what 99% of yank uses look like.
5017
+ killBuffer = "";
5018
+ // Images attached to the current draft. Cleared in the same paths
5019
+ // that clear the text buffer (clearBuffer, after send). Queue
5020
+ // navigation snapshots/restores them alongside savedDraft so up/down
5021
+ // through queued items doesn't drop chips.
5022
+ attachments = [];
5023
+ // Snapshot of `attachments` taken when the user starts walking
5024
+ // history/queue with chips already attached. Restored alongside the
5025
+ // text draft when the walk ends. Distinct from savedDraft because
5026
+ // queue slots (which may carry their own attachments — though we
5027
+ // don't surface that yet) shouldn't blend with the current draft's.
5028
+ savedAttachments = null;
5029
+ constructor(opts = {}) {
5030
+ this.history = [...opts.history ?? []];
5031
+ this.planMode = opts.planMode ?? false;
5001
5032
  }
5002
- return s.cwd === opts.cwd ? 2 : 1;
5003
- };
5004
- return [...sessions].sort((a, b) => {
5005
- const tier = score(b) - score(a);
5006
- if (tier !== 0) {
5007
- return tier;
5033
+ state() {
5034
+ return {
5035
+ buffer: [...this.buffer],
5036
+ row: this.row,
5037
+ col: this.col,
5038
+ planMode: this.planMode,
5039
+ historyIndex: this.historyIndex,
5040
+ queueIndex: this.queueIndex,
5041
+ attachments: [...this.attachments],
5042
+ historySearchQuery: this.historySearch?.query ?? null
5043
+ };
5008
5044
  }
5009
- return b.updatedAt.localeCompare(a.updatedAt);
5010
- });
5011
- };
5012
- let cwdOnly = false;
5013
- let hostFilter = "__local";
5014
- if (opts.currentSessionId !== void 0) {
5015
- const current = opts.sessions.find(
5016
- (s) => s.sessionId === opts.currentSessionId
5017
- );
5018
- if (current?.importedFromMachine) {
5019
- hostFilter = "__all";
5020
- }
5021
- }
5022
- let allSessions = sortSessions(opts.sessions);
5023
- let visible = filterByHost(allSessions, hostFilter);
5024
- let rows = visible.map((s) => toRow(s, Date.now()));
5025
- let widths = computeWidths(rows);
5026
- let total = 1 + visible.length;
5027
- let selectedIdx = 0;
5028
- let scrollOffset = 0;
5029
- if (opts.currentSessionId !== void 0) {
5030
- const idx = visible.findIndex((s) => s.sessionId === opts.currentSessionId);
5031
- if (idx >= 0) {
5032
- selectedIdx = idx + 1;
5033
- }
5034
- }
5035
- let searchActive = false;
5036
- let searchTerm = "";
5037
- let mode = "normal";
5038
- let pendingAction = null;
5039
- let renameBuffer = "";
5040
- let transientStatus = null;
5041
- let termHeight = readTermHeight(term);
5042
- let termWidth = readTermWidth(term);
5043
- let viewportSize = 0;
5044
- let newSessionLabel = "";
5045
- let headerLine = "";
5046
- let sessionLines = [];
5047
- let startRow = 1;
5048
- const cwdMaxWidth = opts.config.tui.cwdColumnMaxWidth;
5049
- const computeLayout = () => {
5050
- termHeight = readTermHeight(term);
5051
- termWidth = readTermWidth(term);
5052
- const maxViewportRows = Math.max(3, termHeight - 6);
5053
- viewportSize = Math.min(visible.length, maxViewportRows);
5054
- const rowMaxWidth = Math.max(10, termWidth - ROW_PREFIX_WIDTH);
5055
- newSessionLabel = formatNewSessionLabel(opts.cwd, rowMaxWidth);
5056
- headerLine = formatRow(HEADER, widths, rowMaxWidth, cwdMaxWidth);
5057
- sessionLines = rows.map((r) => formatRow(r, widths, rowMaxWidth, cwdMaxWidth));
5058
- };
5059
- const rebuildRows = () => {
5060
- rows = visible.map((s) => toRow(s, Date.now()));
5061
- widths = computeWidths(rows);
5062
- total = 1 + visible.length;
5063
- computeLayout();
5064
- };
5065
- const applyFilter = () => {
5066
- let base = allSessions;
5067
- if (cwdOnly) {
5068
- base = base.filter((s) => s.cwd === opts.cwd);
5069
- }
5070
- base = filterByHost(base, hostFilter);
5071
- if (searchActive && searchTerm.length > 0) {
5072
- visible = base.filter((s) => matchesSearch(s, searchTerm));
5073
- } else {
5074
- visible = base;
5075
- }
5076
- rebuildRows();
5077
- if (searchActive) {
5078
- scrollOffset = 0;
5079
- selectedIdx = visible.length > 0 ? 1 : 0;
5080
- } else if (selectedIdx > total - 1) {
5081
- selectedIdx = Math.max(0, total - 1);
5082
- }
5083
- if (scrollOffset + viewportSize > visible.length) {
5084
- scrollOffset = Math.max(0, visible.length - viewportSize);
5085
- }
5086
- adjustScroll();
5087
- };
5088
- const adjustScroll = () => {
5089
- if (selectedIdx === 0) {
5090
- return;
5091
- }
5092
- const sessionIdx = selectedIdx - 1;
5093
- if (sessionIdx < scrollOffset) {
5094
- scrollOffset = sessionIdx;
5095
- } else if (sessionIdx >= scrollOffset + viewportSize) {
5096
- scrollOffset = sessionIdx - viewportSize + 1;
5097
- } else if (scrollOffset + viewportSize > visible.length) {
5098
- scrollOffset = Math.max(0, visible.length - viewportSize);
5099
- }
5100
- };
5101
- const paintNewItem = () => {
5102
- if (selectedIdx === 0) {
5103
- term.brightWhite.bgBlue.noFormat(`\u276F ${newSessionLabel}`);
5104
- } else {
5105
- term.noFormat(` ${newSessionLabel}`);
5106
- }
5107
- };
5108
- const paintSessionRow = (sessionIdx) => {
5109
- const label = sessionLines[sessionIdx] ?? "";
5110
- if (selectedIdx === sessionIdx + 1) {
5111
- term.brightWhite.bgBlue.noFormat(`\u276F ${label}`);
5112
- } else {
5113
- term.noFormat(` ${label}`);
5114
- }
5115
- };
5116
- const formatIndicator = () => {
5117
- const above = scrollOffset;
5118
- const below = Math.max(0, visible.length - scrollOffset - viewportSize);
5119
- const parts = [];
5120
- if (cwdOnly) {
5121
- parts.push("cwd-only");
5122
- }
5123
- if (hostFilter !== "__all") {
5124
- parts.push(
5125
- hostFilter === "__local" ? "host: local" : `host: ${hostFilter}`
5126
- );
5127
- }
5128
- if (above > 0) {
5129
- parts.push(`\u2191 ${above} above`);
5130
- }
5131
- if (below > 0) {
5132
- parts.push(`\u2193 ${below} below`);
5133
- }
5134
- if (parts.length === 0) {
5135
- return "";
5136
- }
5137
- return ` ${parts.join(" \xB7 ")}`;
5138
- };
5139
- const shortId2 = (sessionId) => stripHydraSessionPrefix(sessionId);
5140
- const paintIndicator = () => {
5141
- term.moveTo(1, indicatorRow()).eraseLineAfter();
5142
- if (mode === "confirm-kill" && pendingAction) {
5143
- term.brightYellow.noFormat(` kill ${shortId2(pendingAction.sessionId)}? [y/N]`);
5144
- return;
5145
- }
5146
- if (mode === "confirm-delete" && pendingAction) {
5147
- term.brightRed.noFormat(` delete ${shortId2(pendingAction.sessionId)}? [y/N]`);
5148
- return;
5149
- }
5150
- if (mode === "busy" && pendingAction) {
5151
- term.dim.noFormat(` working on ${shortId2(pendingAction.sessionId)}\u2026`);
5152
- return;
5153
- }
5154
- if (mode === "rename" && pendingAction) {
5155
- term.brightYellow.noFormat(` title: ${renameBuffer}`);
5156
- term.bgBrightYellow(" ");
5157
- term.dim.noFormat(" Enter saves \xB7 Esc cancels");
5158
- return;
5159
- }
5160
- if (transientStatus !== null) {
5161
- term.dim.noFormat(` ${transientStatus}`);
5162
- return;
5163
- }
5164
- if (searchActive) {
5165
- term.brightYellow.noFormat(` /${searchTerm}`);
5166
- term.bgBrightYellow(" ");
5167
- const hint = visible.length === 0 ? " no matches" : ` ${visible.length} match${visible.length === 1 ? "" : "es"}`;
5168
- term.dim.noFormat(`${hint} \xB7 ^c clears`);
5169
- return;
5170
- }
5171
- term.dim.noFormat(formatIndicator());
5172
- };
5173
- const indicatorRow = () => startRow + 3 + viewportSize;
5174
- const sessionRow = (sessionIdx) => startRow + 3 + (sessionIdx - scrollOffset);
5175
- const renderFromScratch = () => {
5176
- if (mode === "help") {
5177
- renderHelp();
5178
- return;
5179
- }
5180
- computeLayout();
5181
- adjustScroll();
5182
- startRow = 1;
5183
- term.moveTo(1, 1).eraseDisplayBelow();
5184
- paintNewItem();
5185
- term("\n\n");
5186
- term.dim.noFormat(` ${headerLine}`)("\n");
5187
- for (let v = 0; v < viewportSize; v++) {
5188
- paintSessionRow(scrollOffset + v);
5189
- term("\n");
5190
- }
5191
- paintIndicator();
5192
- term("\n");
5193
- };
5194
- const renderHelp = () => {
5195
- term.moveTo(1, 1).eraseDisplayBelow();
5196
- term.brightWhite.bold.noFormat(" Picker hotkeys")("\n\n");
5197
- for (const entry of HELP_ENTRIES) {
5198
- if (entry === null) {
5199
- term("\n");
5200
- continue;
5201
- }
5202
- const [keys, desc] = entry;
5203
- term.brightCyan.noFormat(` ${keys.padEnd(HELP_KEYS_WIDTH)}`);
5204
- term.noFormat(desc)("\n");
5205
- }
5206
- term("\n");
5207
- term.dim.noFormat(" press any key to dismiss")("\n");
5208
- };
5209
- const repaintNewItem = () => {
5210
- term.moveTo(1, startRow).eraseLineAfter();
5211
- paintNewItem();
5212
- };
5213
- const repaintSessionRow = (sessionIdx) => {
5214
- if (sessionIdx < scrollOffset || sessionIdx >= scrollOffset + viewportSize) {
5215
- return;
5216
- }
5217
- term.moveTo(1, sessionRow(sessionIdx)).eraseLineAfter();
5218
- paintSessionRow(sessionIdx);
5219
- };
5220
- const repaintViewport = () => {
5221
- for (let v = 0; v < viewportSize; v++) {
5222
- const row = startRow + 3 + v;
5223
- term.moveTo(1, row).eraseLineAfter();
5224
- const sessionIdx = scrollOffset + v;
5225
- if (sessionIdx < visible.length) {
5226
- paintSessionRow(sessionIdx);
5227
- }
5228
- }
5229
- paintIndicator();
5230
- };
5231
- renderFromScratch();
5232
- term.hideCursor();
5233
- return await new Promise((resolve6) => {
5234
- let resolved = false;
5235
- const onResize = () => {
5236
- if (resolved) {
5237
- return;
5238
- }
5239
- renderFromScratch();
5240
- };
5241
- const cleanup = () => {
5242
- if (resolved) {
5243
- return;
5045
+ // App calls this after asynchronously acquiring an image (drag-drop
5046
+ // file read, clipboard shellout). The dispatcher just records it;
5047
+ // chip rendering and capability gating live in the app/screen layer.
5048
+ addAttachment(attachment) {
5049
+ this.attachments.push(attachment);
5244
5050
  }
5245
- resolved = true;
5246
- term.off("key", onKey);
5247
- term.off("resize", onResize);
5248
- term.grabInput(false);
5249
- term.hideCursor(false);
5250
- term.moveTo(1, indicatorRow() + 1);
5251
- term("\n");
5252
- };
5253
- const refresh = async (preferredId) => {
5254
- try {
5255
- const next = await listSessions(opts.target);
5256
- allSessions = sortSessions(next);
5257
- applyFilter();
5258
- if (preferredId !== void 0) {
5259
- const idx = visible.findIndex((s) => s.sessionId === preferredId);
5260
- if (idx >= 0) {
5261
- selectedIdx = idx + 1;
5262
- }
5263
- }
5264
- if (selectedIdx > total - 1) {
5265
- selectedIdx = Math.max(0, total - 1);
5266
- }
5267
- if (scrollOffset + viewportSize > visible.length) {
5268
- scrollOffset = Math.max(0, visible.length - viewportSize);
5051
+ removeAttachment(index) {
5052
+ if (index < 0 || index >= this.attachments.length) {
5053
+ return;
5269
5054
  }
5270
- adjustScroll();
5271
- renderFromScratch();
5272
- } catch (err) {
5273
- transientStatus = `refresh failed: ${err.message}`;
5274
- renderFromScratch();
5275
- }
5276
- };
5277
- const performRename = async (title) => {
5278
- if (!pendingAction) {
5279
- return;
5280
- }
5281
- const session = pendingAction;
5282
- mode = "busy";
5283
- paintIndicator();
5284
- try {
5285
- await renameSession(opts.target, session.sessionId, title);
5286
- mode = "normal";
5287
- pendingAction = null;
5288
- renameBuffer = "";
5289
- await refresh(session.sessionId);
5290
- } catch (err) {
5291
- mode = "normal";
5292
- pendingAction = null;
5293
- renameBuffer = "";
5294
- transientStatus = `rename failed: ${err.message}`;
5295
- paintIndicator();
5055
+ this.attachments.splice(index, 1);
5296
5056
  }
5297
- };
5298
- const performRegen = async (session) => {
5299
- try {
5300
- await regenSessionTitle(opts.target, session.sessionId);
5301
- transientStatus = "title regen queued (press r to refresh)";
5302
- paintIndicator();
5303
- } catch (err) {
5304
- transientStatus = `regen failed: ${err.message}`;
5305
- paintIndicator();
5057
+ setTurnRunning(running) {
5058
+ this.turnRunning = running;
5306
5059
  }
5307
- };
5308
- const performAction = async (kind) => {
5309
- if (!pendingAction) {
5310
- return;
5060
+ setHistory(history) {
5061
+ this.history = [...history];
5062
+ this.historyIndex = -1;
5063
+ this.savedDraft = null;
5064
+ this.historySearch = null;
5311
5065
  }
5312
- const session = pendingAction;
5313
- mode = "busy";
5314
- paintIndicator();
5315
- try {
5316
- if (kind === "kill") {
5317
- await killSession(opts.target, session.sessionId);
5318
- } else {
5319
- await deleteSession(opts.target, session.sessionId);
5066
+ // Snapshot of the waiting queue (head excluded). Called by the app after
5067
+ // every queue mutation so Up/Down can walk a fresh view. queueIndex is
5068
+ // only invalidated when it falls outside the new bounds — staying in
5069
+ // bounds preserves the user's edit if the queue grew or stayed put.
5070
+ setQueue(queue) {
5071
+ this.queue = [...queue];
5072
+ if (this.queueIndex >= this.queue.length) {
5073
+ this.queueIndex = -1;
5320
5074
  }
5321
- mode = "normal";
5322
- pendingAction = null;
5323
- await refresh(kind === "kill" ? session.sessionId : void 0);
5324
- } catch (err) {
5325
- mode = "normal";
5326
- pendingAction = null;
5327
- transientStatus = `${kind} failed: ${err.message}`;
5328
- paintIndicator();
5329
5075
  }
5330
- };
5331
- const move = (delta) => {
5332
- const next = Math.min(total - 1, Math.max(0, selectedIdx + delta));
5333
- if (next === selectedIdx) {
5334
- return;
5335
- }
5336
- const old = selectedIdx;
5337
- const oldScroll = scrollOffset;
5338
- selectedIdx = next;
5339
- adjustScroll();
5340
- if (scrollOffset !== oldScroll) {
5341
- repaintViewport();
5342
- if (old === 0 || selectedIdx === 0) {
5343
- repaintNewItem();
5076
+ // Replace the contents of the first row, leaving subsequent rows alone.
5077
+ // Used by slash-command completion: the partial /foo gets swapped for the
5078
+ // matched command name. Cursor moves to the end of the replacement.
5079
+ replaceFirstLine(text) {
5080
+ this.buffer[0] = text;
5081
+ if (this.row === 0) {
5082
+ this.col = text.length;
5344
5083
  }
5345
- return;
5346
- }
5347
- if (old === 0) {
5348
- repaintNewItem();
5349
- } else {
5350
- repaintSessionRow(old - 1);
5351
- }
5352
- if (selectedIdx === 0) {
5353
- repaintNewItem();
5354
- } else {
5355
- repaintSessionRow(selectedIdx - 1);
5356
- }
5357
- };
5358
- const clearTransient = () => {
5359
- if (transientStatus === null) {
5360
- return false;
5361
- }
5362
- transientStatus = null;
5363
- paintIndicator();
5364
- return true;
5365
- };
5366
- const onKey = (name, _matches, data) => {
5367
- if (mode === "busy") {
5368
- return;
5369
5084
  }
5370
- if (mode === "help") {
5371
- if (name === "CTRL_C") {
5372
- cleanup();
5373
- resolve6({ kind: "abort" });
5374
- return;
5375
- }
5376
- mode = "normal";
5377
- renderFromScratch();
5378
- return;
5085
+ // Public seed for the buffer (used for Escape pre-fill). Treated like a
5086
+ // fresh draft: nav state and any saved draft are cleared, cursor lands
5087
+ // at the end so the user can edit immediately. Attachments restore
5088
+ // alongside the text so a cancelled turn's chips land back in the
5089
+ // draft together with the typed prompt.
5090
+ setBuffer(text, attachments = []) {
5091
+ this.loadEntry(text);
5092
+ this.historyIndex = -1;
5093
+ this.queueIndex = -1;
5094
+ this.savedDraft = null;
5095
+ this.savedAttachments = null;
5096
+ this.historySearch = null;
5097
+ this.attachments = [...attachments];
5379
5098
  }
5380
- if (mode === "rename") {
5381
- if (name === "ENTER" || name === "KP_ENTER") {
5382
- const trimmed = renameBuffer.trim();
5383
- if (trimmed.length === 0) {
5384
- mode = "normal";
5385
- pendingAction = null;
5386
- renameBuffer = "";
5387
- paintIndicator();
5388
- return;
5099
+ feed(event) {
5100
+ if (this.historySearch !== null) {
5101
+ if (event.type === "char") {
5102
+ return this.mutateHistorySearchQuery(
5103
+ this.historySearch.query + event.ch.toLowerCase()
5104
+ );
5389
5105
  }
5390
- void performRename(trimmed);
5391
- return;
5392
- }
5393
- if (name === "ESCAPE" || name === "CTRL_C") {
5394
- mode = "normal";
5395
- pendingAction = null;
5396
- renameBuffer = "";
5397
- paintIndicator();
5398
- return;
5399
- }
5400
- if (name === "BACKSPACE") {
5401
- if (renameBuffer.length > 0) {
5402
- renameBuffer = renameBuffer.slice(0, -1);
5403
- paintIndicator();
5106
+ if (event.type === "paste") {
5107
+ return this.mutateHistorySearchQuery(
5108
+ this.historySearch.query + event.text.replace(/\n/g, " ").toLowerCase()
5109
+ );
5404
5110
  }
5405
- return;
5406
- }
5407
- if (name === "CTRL_U") {
5408
- renameBuffer = "";
5409
- paintIndicator();
5410
- return;
5411
- }
5412
- if (name === "CTRL_W") {
5413
- const trimmedRight = renameBuffer.replace(/\s+$/, "");
5414
- const lastSpace = trimmedRight.lastIndexOf(" ");
5415
- renameBuffer = lastSpace >= 0 ? trimmedRight.slice(0, lastSpace) : "";
5416
- paintIndicator();
5417
- return;
5418
- }
5419
- if (data?.isCharacter) {
5420
- renameBuffer += name;
5421
- paintIndicator();
5422
- return;
5423
- }
5424
- return;
5425
- }
5426
- if (mode === "confirm-kill" || mode === "confirm-delete") {
5427
- if (data?.isCharacter && (name === "y" || name === "Y")) {
5428
- const kind = mode === "confirm-kill" ? "kill" : "delete";
5429
- void performAction(kind);
5430
- return;
5431
- }
5432
- if (name === "ESCAPE" || name === "CTRL_C" || name === "ENTER" || name === "KP_ENTER" || data?.isCharacter && (name === "n" || name === "N")) {
5433
- mode = "normal";
5434
- pendingAction = null;
5435
- paintIndicator();
5436
- return;
5437
- }
5438
- return;
5439
- }
5440
- clearTransient();
5441
- if (!searchActive && data?.isCharacter && name === "?") {
5442
- mode = "help";
5443
- renderHelp();
5444
- return;
5445
- }
5446
- if (searchActive) {
5447
- if (data?.isCharacter) {
5448
- searchTerm += name;
5449
- applyFilter();
5450
- renderFromScratch();
5451
- return;
5452
- }
5453
- if (name === "BACKSPACE") {
5454
- if (searchTerm.length > 0) {
5455
- searchTerm = searchTerm.slice(0, -1);
5456
- applyFilter();
5457
- renderFromScratch();
5458
- } else {
5459
- searchActive = false;
5460
- applyFilter();
5461
- renderFromScratch();
5462
- }
5463
- return;
5464
- }
5465
- if (name === "ESCAPE" || name === "CTRL_C") {
5466
- searchActive = false;
5467
- searchTerm = "";
5468
- applyFilter();
5469
- renderFromScratch();
5470
- return;
5471
- }
5472
- }
5473
- if (data?.isCharacter) {
5474
- if (name === "/") {
5475
- searchActive = true;
5476
- searchTerm = "";
5477
- applyFilter();
5478
- renderFromScratch();
5479
- return;
5480
- }
5481
- if (name === "n" || name === "N") {
5482
- move(1);
5483
- return;
5484
- }
5485
- if (name === "p" || name === "P") {
5486
- move(-1);
5487
- return;
5488
- }
5489
- if (name === "c" || name === "C") {
5490
- cleanup();
5491
- resolve6({ kind: "new" });
5492
- return;
5493
- }
5494
- if (name === "q" || name === "Q") {
5495
- cleanup();
5496
- resolve6({ kind: "abort" });
5497
- return;
5498
- }
5499
- if (name === "o" || name === "O") {
5500
- const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
5501
- cwdOnly = !cwdOnly;
5502
- applyFilter();
5503
- if (keepId !== void 0) {
5504
- const idx = visible.findIndex((s) => s.sessionId === keepId);
5505
- if (idx >= 0) {
5506
- selectedIdx = idx + 1;
5507
- adjustScroll();
5111
+ if (event.type === "key") {
5112
+ if (event.name === "ctrl-r") {
5113
+ return this.advanceHistorySearch();
5508
5114
  }
5509
- }
5510
- renderFromScratch();
5511
- return;
5512
- }
5513
- if (name === "h" || name === "H") {
5514
- const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
5515
- hostFilter = nextHostFilter(hostFilter, allSessions);
5516
- applyFilter();
5517
- if (keepId !== void 0) {
5518
- const idx = visible.findIndex((s) => s.sessionId === keepId);
5519
- if (idx >= 0) {
5520
- selectedIdx = idx + 1;
5521
- adjustScroll();
5115
+ if (event.name === "ctrl-s") {
5116
+ this.retreatHistorySearch();
5117
+ return [];
5522
5118
  }
5119
+ if (event.name === "escape" || event.name === "ctrl-c") {
5120
+ this.cancelHistorySearch();
5121
+ return [];
5122
+ }
5123
+ if (event.name === "backspace") {
5124
+ if (this.historySearch.query.length === 0) {
5125
+ this.cancelHistorySearch();
5126
+ return [];
5127
+ }
5128
+ return this.mutateHistorySearchQuery(
5129
+ this.historySearch.query.slice(0, -1)
5130
+ );
5131
+ }
5132
+ this.historySearch = null;
5523
5133
  }
5524
- renderFromScratch();
5525
- return;
5526
- }
5527
- if (name === "r" || name === "R") {
5528
- const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
5529
- void refresh(currentId);
5530
- return;
5531
- }
5532
- if ((name === "v" || name === "V") && selectedIdx > 0) {
5533
- const session = visible[selectedIdx - 1];
5534
- if (!session) {
5535
- return;
5536
- }
5537
- cleanup();
5538
- const result = {
5539
- kind: "attach",
5540
- sessionId: session.sessionId,
5541
- readonly: true
5542
- };
5543
- if (session.agentId !== void 0) {
5544
- result.agentId = session.agentId;
5545
- }
5546
- resolve6(result);
5547
- return;
5548
- }
5549
- if ((name === "k" || name === "K") && selectedIdx > 0) {
5550
- const session = visible[selectedIdx - 1];
5551
- if (!session) {
5552
- return;
5553
- }
5554
- pendingAction = {
5555
- sessionId: session.sessionId,
5556
- cwd: session.cwd,
5557
- status: session.status
5558
- };
5559
- mode = "confirm-kill";
5560
- paintIndicator();
5561
- return;
5562
5134
  }
5563
- if (name === "t" && selectedIdx > 0) {
5564
- const session = visible[selectedIdx - 1];
5565
- if (!session) {
5566
- return;
5567
- }
5568
- pendingAction = {
5569
- sessionId: session.sessionId,
5570
- cwd: session.cwd,
5571
- status: session.status
5572
- };
5573
- renameBuffer = session.title ?? "";
5574
- mode = "rename";
5575
- paintIndicator();
5576
- return;
5135
+ if (event.type === "char") {
5136
+ this.insertChar(event.ch);
5137
+ return [];
5577
5138
  }
5578
- if (name === "T" && selectedIdx > 0) {
5579
- const session = visible[selectedIdx - 1];
5580
- if (!session || session.status !== "live") {
5581
- return;
5582
- }
5583
- void performRegen({ sessionId: session.sessionId });
5584
- return;
5139
+ if (event.type === "paste") {
5140
+ this.insertText(event.text);
5141
+ return [];
5585
5142
  }
5586
- if ((name === "d" || name === "D") && selectedIdx > 0) {
5587
- const session = visible[selectedIdx - 1];
5588
- if (!session) {
5589
- return;
5590
- }
5591
- if (session.status === "live") {
5592
- transientStatus = "session is live \u2014 press k to kill it first";
5593
- paintIndicator();
5594
- return;
5595
- }
5596
- pendingAction = {
5597
- sessionId: session.sessionId,
5598
- cwd: session.cwd,
5599
- status: session.status
5600
- };
5601
- mode = "confirm-delete";
5602
- paintIndicator();
5603
- return;
5143
+ if (event.type === "attachment-paths") {
5144
+ return [];
5604
5145
  }
5605
- return;
5146
+ return this.handleKey(event.name);
5606
5147
  }
5607
- switch (name) {
5608
- case "UP":
5609
- case "SHIFT_TAB":
5610
- move(-1);
5611
- return;
5612
- case "DOWN":
5613
- case "TAB":
5614
- move(1);
5615
- return;
5616
- case "PAGE_UP":
5617
- move(-viewportSize);
5618
- return;
5619
- case "PAGE_DOWN":
5620
- move(viewportSize);
5621
- return;
5622
- case "HOME":
5623
- move(1 - selectedIdx);
5624
- return;
5625
- case "END":
5626
- move(total);
5627
- return;
5628
- case "ENTER":
5629
- case "KP_ENTER": {
5630
- cleanup();
5631
- if (selectedIdx === 0) {
5632
- resolve6({ kind: "new" });
5633
- return;
5634
- }
5635
- const session = visible[selectedIdx - 1];
5636
- if (!session) {
5637
- resolve6({ kind: "abort" });
5638
- return;
5639
- }
5640
- const result = {
5641
- kind: "attach",
5642
- sessionId: session.sessionId
5643
- };
5644
- if (session.agentId !== void 0) {
5645
- result.agentId = session.agentId;
5646
- }
5647
- resolve6(result);
5648
- return;
5649
- }
5650
- case "ESCAPE":
5651
- case "CTRL_C":
5652
- case "CTRL_D":
5653
- cleanup();
5654
- resolve6({ kind: "abort" });
5655
- return;
5656
- }
5657
- };
5658
- term.grabInput({});
5659
- term.on("key", onKey);
5660
- term.on("resize", onResize);
5661
- });
5662
- }
5663
- function readTermHeight(term) {
5664
- return term.height ?? 24;
5665
- }
5666
- function readTermWidth(term) {
5667
- return term.width ?? 80;
5668
- }
5669
- function formatNewSessionLabel(cwd, maxWidth) {
5670
- const prefix = "New session in ";
5671
- const budget = Math.max(1, maxWidth - prefix.length);
5672
- return prefix + truncateMiddle(shortenHomePath(cwd), budget);
5673
- }
5674
- function filterByHost(sessions, hostFilter) {
5675
- if (hostFilter === "__all") {
5676
- return sessions;
5677
- }
5678
- if (hostFilter === "__local") {
5679
- return sessions.filter(
5680
- (s) => !s.importedFromMachine || !!s.upstreamSessionId
5681
- );
5682
- }
5683
- return sessions.filter(
5684
- (s) => s.importedFromMachine === hostFilter && !s.upstreamSessionId
5685
- );
5686
- }
5687
- function nextHostFilter(current, sessions) {
5688
- const hosts = /* @__PURE__ */ new Set();
5689
- for (const s of sessions) {
5690
- if (s.importedFromMachine && !s.upstreamSessionId) {
5691
- hosts.add(s.importedFromMachine);
5692
- }
5693
- }
5694
- const ordered = ["__local", ...[...hosts].sort(), "__all"];
5695
- const idx = ordered.indexOf(current);
5696
- if (idx === -1) {
5697
- return "__local";
5698
- }
5699
- return ordered[(idx + 1) % ordered.length] ?? "__local";
5700
- }
5701
- function matchesSearch(s, term) {
5702
- if (term.length === 0) {
5703
- return true;
5704
- }
5705
- const t = term.toLowerCase();
5706
- const haystacks = [
5707
- stripHydraSessionPrefix(s.sessionId),
5708
- s.upstreamSessionId ?? "",
5709
- s.agentId ?? "",
5710
- s.title ?? "",
5711
- s.cwd,
5712
- shortenHomePath(s.cwd)
5713
- ];
5714
- for (const h of haystacks) {
5715
- if (h.toLowerCase().includes(t)) {
5716
- return true;
5717
- }
5718
- }
5719
- return false;
5720
- }
5721
- var ROW_PREFIX_WIDTH, HELP_KEYS_WIDTH, HELP_ENTRIES;
5722
- var init_picker = __esm({
5723
- "src/tui/picker.ts"() {
5724
- "use strict";
5725
- init_session_row();
5726
- init_paths();
5727
- init_session();
5728
- init_discovery();
5729
- ROW_PREFIX_WIDTH = 2;
5730
- HELP_KEYS_WIDTH = 20;
5731
- HELP_ENTRIES = [
5732
- ["\u2191 / \u2193 or n / p", "navigate"],
5733
- ["PgUp / PgDn", "page up / page down"],
5734
- ["Home / End", "first / last"],
5735
- ["Enter", "open selected session (or create new)"],
5736
- ["v", "view-only (open transcript without spawning the agent)"],
5737
- null,
5738
- ["/", "search sessions"],
5739
- ["o", "toggle cwd-only filter"],
5740
- ["h", "cycle host filter (local / <peer> / all)"],
5741
- ["r", "refresh from daemon"],
5742
- null,
5743
- ["k", "kill the selected live session"],
5744
- ["d", "delete the selected cold session"],
5745
- ["t", "retitle the selected session"],
5746
- ["T", "regenerate title via agent (live session)"],
5747
- null,
5748
- ["c", "create new session"],
5749
- ["?", "toggle this help"],
5750
- ["q / Esc / ^C / ^D", "quit picker (detach)"]
5751
- ];
5752
- }
5753
- });
5754
-
5755
- // src/core/cwd.ts
5756
- import * as fs19 from "fs/promises";
5757
- import * as path13 from "path";
5758
- async function validateLocalCwd(input) {
5759
- const trimmed = input.trim();
5760
- if (trimmed.length === 0) {
5761
- return { ok: false, reason: "path is empty" };
5762
- }
5763
- const resolved = path13.resolve(expandHome(trimmed));
5764
- let stat5;
5765
- try {
5766
- stat5 = await fs19.stat(resolved);
5767
- } catch {
5768
- return { ok: false, reason: `${resolved} does not exist` };
5769
- }
5770
- if (!stat5.isDirectory()) {
5771
- return { ok: false, reason: `${resolved} is not a directory` };
5772
- }
5773
- return { ok: true, path: resolved };
5774
- }
5775
- var init_cwd = __esm({
5776
- "src/core/cwd.ts"() {
5777
- "use strict";
5778
- init_config();
5779
- }
5780
- });
5781
-
5782
- // src/tui/prompt-utils.ts
5783
- function resetTerminalModes() {
5784
- process.stdout.write("\x1B[<u");
5785
- process.stdout.write("\x1B[?2004l");
5786
- process.stdout.write("\x1B[>4;0m");
5787
- process.stdout.write("\x1B[>5;0m");
5788
- process.stdout.write("\x1B[?1000l");
5789
- process.stdout.write("\x1B[?1002l");
5790
- process.stdout.write("\x1B[?1006l");
5791
- process.stdout.write("\x1B[?1l");
5792
- process.stdout.write("\x1B>");
5793
- }
5794
- function readTermWidth2(term) {
5795
- return term.width ?? 80;
5796
- }
5797
- function readTermHeight2(term) {
5798
- return term.height ?? 24;
5799
- }
5800
- function drawBox(term, opts) {
5801
- const termW = readTermWidth2(term);
5802
- const termH = readTermHeight2(term);
5803
- const desiredContentW = opts.contentWidth ?? MAX_BOX_WIDTH;
5804
- const maxContentW = Math.max(10, Math.min(MAX_BOX_WIDTH, termW - 4));
5805
- const contentW = Math.min(desiredContentW, maxContentW);
5806
- const w = contentW + 2;
5807
- const contentH = Math.max(1, Math.min(opts.contentHeight, termH - 4));
5808
- const h = contentH + 2;
5809
- const x = Math.max(1, Math.floor((termW - w) / 2) + 1);
5810
- const y = Math.max(1, Math.floor((termH - h) / 2) + 1);
5811
- term.moveTo(1, 1).eraseDisplayBelow();
5812
- const topInner = HORIZ.repeat(w - 2);
5813
- const top = renderTitleStrip(topInner, opts.title);
5814
- term.moveTo(x, y);
5815
- term.dim.noFormat(TL);
5816
- paintTopStrip(term, top);
5817
- term.dim.noFormat(TR);
5818
- for (let row = 1; row <= contentH; row++) {
5819
- term.moveTo(x, y + row);
5820
- term.dim.noFormat(VERT);
5821
- term.moveTo(x + w - 1, y + row);
5822
- term.dim.noFormat(VERT);
5823
- }
5824
- term.moveTo(x, y + h - 1);
5825
- term.dim.noFormat(BL + HORIZ.repeat(w - 2) + BR);
5826
- return {
5827
- x,
5828
- y,
5829
- w,
5830
- h,
5831
- contentX: x + 1,
5832
- contentY: y + 1,
5833
- contentW,
5834
- contentH
5835
- };
5836
- }
5837
- function renderTitleStrip(innerDashes, title) {
5838
- if (!title) {
5839
- return { dashes: innerDashes };
5840
- }
5841
- const chip = ` ${title} `;
5842
- if (chip.length + 4 > innerDashes.length) {
5843
- return { dashes: innerDashes };
5844
- }
5845
- const offset = 2;
5846
- const dashes = innerDashes.slice(0, offset) + " ".repeat(chip.length) + innerDashes.slice(offset + chip.length);
5847
- return { dashes, title: { offset, text: chip } };
5848
- }
5849
- function paintTopStrip(term, strip) {
5850
- if (!strip.title) {
5851
- term.dim.noFormat(strip.dashes);
5852
- return;
5853
- }
5854
- term.dim.noFormat(strip.dashes.slice(0, strip.title.offset));
5855
- term.brightCyan.noFormat(strip.title.text);
5856
- term.dim.noFormat(strip.dashes.slice(strip.title.offset + strip.title.text.length));
5857
- }
5858
- var MAX_BOX_WIDTH, HORIZ, VERT, TL, TR, BL, BR;
5859
- var init_prompt_utils = __esm({
5860
- "src/tui/prompt-utils.ts"() {
5861
- "use strict";
5862
- MAX_BOX_WIDTH = 64;
5863
- HORIZ = "\u2500";
5864
- VERT = "\u2502";
5865
- TL = "\u250C";
5866
- TR = "\u2510";
5867
- BL = "\u2514";
5868
- BR = "\u2518";
5869
- }
5870
- });
5871
-
5872
- // src/tui/import-cwd-prompt.ts
5873
- import * as os5 from "os";
5874
- async function promptForImportCwd(term, session, opts = {}) {
5875
- const defaultCwd = opts.defaultCwd ?? os5.homedir();
5876
- resetTerminalModes();
5877
- const shortId2 = stripHydraSessionPrefix(session.sessionId);
5878
- const fromMachine = session.importedFromMachine ?? "another machine";
5879
- const originalCwd = shortenHomePath(session.cwd);
5880
- let buffer = defaultCwd;
5881
- let errorLine = null;
5882
- let busy = false;
5883
- let layout = null;
5884
- const render = () => {
5885
- const contentHeight = 9;
5886
- layout = drawBox(term, {
5887
- contentHeight,
5888
- title: "Run locally \u2014 choose cwd"
5889
- });
5890
- const innerW = layout.contentW;
5891
- const headerRows = [
5892
- { label: "session: ", value: shortId2 },
5893
- { label: "from: ", value: fromMachine },
5894
- { label: "cwd: ", value: originalCwd }
5895
- ];
5896
- let row = 0;
5897
- for (const hr of headerRows) {
5898
- term.moveTo(layout.contentX, layout.contentY + row);
5899
- term.dim.noFormat(` ${hr.label}`);
5900
- term.noFormat(truncate(hr.value, innerW - hr.label.length - 2));
5901
- row++;
5902
- }
5903
- row++;
5904
- term.moveTo(layout.contentX, layout.contentY + row);
5905
- term.noFormat(" Pick a local cwd for this session:");
5906
- row += 2;
5907
- paintInputRow(row);
5908
- row += 2;
5909
- if (errorLine !== null) {
5910
- term.moveTo(layout.contentX, layout.contentY + row);
5911
- term.red.noFormat(` ${truncate(errorLine, innerW - 2)}`);
5912
- } else {
5913
- term.moveTo(layout.contentX, layout.contentY + row);
5914
- term.dim.noFormat(
5915
- " Enter accept \xB7 Esc back \xB7 ^U clear \xB7 ^W kill word"
5916
- );
5917
- }
5918
- };
5919
- const inputRow = () => 7;
5920
- const paintInputRow = (rowOffset) => {
5921
- if (!layout) {
5922
- return;
5923
- }
5924
- const r = rowOffset ?? inputRow();
5925
- term.moveTo(layout.contentX, layout.contentY + r).eraseLineAfter();
5926
- term.moveTo(layout.x + layout.w - 1, layout.contentY + r);
5927
- term.dim.noFormat("\u2502");
5928
- term.moveTo(layout.contentX, layout.contentY + r);
5929
- term.bold.noFormat(" cwd: ");
5930
- const available = layout.contentW - " cwd: ".length - 2;
5931
- term.noFormat(truncateLeft(buffer, available));
5932
- if (!busy) {
5933
- term.bgWhite(" ");
5934
- }
5935
- };
5936
- const repaintInput = () => {
5937
- paintInputRow();
5938
- if (!layout) {
5939
- return;
5940
- }
5941
- const errRow = inputRow() + 2;
5942
- term.moveTo(layout.contentX, layout.contentY + errRow).eraseLineAfter();
5943
- term.moveTo(layout.x + layout.w - 1, layout.contentY + errRow);
5944
- term.dim.noFormat("\u2502");
5945
- term.moveTo(layout.contentX, layout.contentY + errRow);
5946
- if (errorLine !== null) {
5947
- term.red.noFormat(` ${truncate(errorLine, layout.contentW - 2)}`);
5948
- } else {
5949
- term.dim.noFormat(
5950
- " Enter accept \xB7 Esc back \xB7 ^U clear \xB7 ^W kill word"
5951
- );
5952
- }
5953
- };
5954
- render();
5955
- return await new Promise((resolve6) => {
5956
- let resolved = false;
5957
- const cleanup = () => {
5958
- if (resolved) {
5959
- return;
5148
+ handleKey(name) {
5149
+ switch (name) {
5150
+ case "enter":
5151
+ return this.send();
5152
+ case "shift-enter":
5153
+ case "ctrl-enter":
5154
+ return this.amend();
5155
+ case "alt-enter":
5156
+ this.insertNewline();
5157
+ return [];
5158
+ case "shift-tab":
5159
+ this.planMode = !this.planMode;
5160
+ return [
5161
+ { type: "plan-toggle", on: this.planMode },
5162
+ { type: "redraw-banner" }
5163
+ ];
5164
+ case "tab":
5165
+ this.insertText(" ");
5166
+ return [];
5167
+ case "up":
5168
+ return this.handleUp();
5169
+ case "down":
5170
+ return this.handleDown();
5171
+ case "left":
5172
+ this.moveLeft();
5173
+ return [];
5174
+ case "right":
5175
+ this.moveRight();
5176
+ return [];
5177
+ case "ctrl-a":
5178
+ this.col = 0;
5179
+ return [];
5180
+ case "ctrl-e":
5181
+ this.col = this.currentLine().length;
5182
+ return [];
5183
+ case "home":
5184
+ return this.handleHome();
5185
+ case "end":
5186
+ return this.handleEnd();
5187
+ case "ctrl-b":
5188
+ this.moveLeft();
5189
+ return [];
5190
+ case "ctrl-f":
5191
+ this.moveRight();
5192
+ return [];
5193
+ case "ctrl-g":
5194
+ return [{ type: "show-help" }];
5195
+ case "alt-b":
5196
+ this.moveWordBackward();
5197
+ return [];
5198
+ case "alt-f":
5199
+ this.moveWordForward();
5200
+ return [];
5201
+ case "ctrl-k":
5202
+ this.killToEnd();
5203
+ return [];
5204
+ case "ctrl-n":
5205
+ return this.handleDown();
5206
+ case "ctrl-o":
5207
+ return [{ type: "toggle-tools" }];
5208
+ case "backspace":
5209
+ this.backspace();
5210
+ return [];
5211
+ case "delete":
5212
+ this.deleteForward();
5213
+ return [];
5214
+ case "ctrl-c":
5215
+ return this.handleCtrlC();
5216
+ case "ctrl-d":
5217
+ if (this.bufferIsEmpty()) {
5218
+ return [{ type: "exit" }];
5219
+ }
5220
+ this.deleteForward();
5221
+ return [];
5222
+ case "ctrl-l":
5223
+ return [{ type: "redraw" }];
5224
+ case "ctrl-p":
5225
+ return [{ type: "switch-session" }];
5226
+ case "ctrl-t":
5227
+ return [{ type: "next-live-session" }];
5228
+ case "ctrl-r":
5229
+ return this.startHistorySearch();
5230
+ case "ctrl-s":
5231
+ return [];
5232
+ case "ctrl-u":
5233
+ this.killLine();
5234
+ return [];
5235
+ case "ctrl-v":
5236
+ return [{ type: "attachment-request", source: "clipboard" }];
5237
+ case "ctrl-w":
5238
+ this.killWord();
5239
+ return [];
5240
+ case "ctrl-x":
5241
+ return [{ type: "toggle-mouse" }];
5242
+ case "ctrl-y":
5243
+ this.yank();
5244
+ return [];
5245
+ case "escape":
5246
+ if (this.turnRunning) {
5247
+ return [{ type: "cancel", prefill: true }];
5248
+ }
5249
+ return [];
5250
+ }
5960
5251
  }
5961
- resolved = true;
5962
- term.off("key", onKey);
5963
- term.off("resize", onResize);
5964
- term.grabInput(false);
5965
- term.hideCursor(false);
5966
- term.moveTo(1, 1).eraseDisplayBelow();
5967
- };
5968
- const finish = (value) => {
5969
- cleanup();
5970
- resolve6(value);
5971
- };
5972
- const onResize = () => {
5973
- if (resolved) {
5974
- return;
5252
+ currentLine() {
5253
+ return this.buffer[this.row] ?? "";
5975
5254
  }
5976
- render();
5977
- };
5978
- const onKey = (name, _matches, data) => {
5979
- if (busy) {
5980
- return;
5255
+ setCurrentLine(line) {
5256
+ this.buffer[this.row] = line;
5257
+ }
5258
+ bufferText() {
5259
+ return this.buffer.join("\n");
5260
+ }
5261
+ bufferIsEmpty() {
5262
+ return this.buffer.length === 1 && this.buffer[0] === "";
5263
+ }
5264
+ clearBuffer() {
5265
+ this.buffer = [""];
5266
+ this.row = 0;
5267
+ this.col = 0;
5268
+ this.historyIndex = -1;
5269
+ this.queueIndex = -1;
5270
+ this.savedDraft = null;
5271
+ this.savedAttachments = null;
5272
+ this.historySearch = null;
5273
+ this.attachments = [];
5274
+ }
5275
+ insertChar(ch) {
5276
+ if (ch.length === 0) {
5277
+ return;
5278
+ }
5279
+ if (ch.includes("\n")) {
5280
+ this.insertText(ch);
5281
+ return;
5282
+ }
5283
+ const line = this.currentLine();
5284
+ this.setCurrentLine(line.slice(0, this.col) + ch + line.slice(this.col));
5285
+ this.col += ch.length;
5286
+ }
5287
+ insertText(text) {
5288
+ const lines = text.split("\n");
5289
+ if (lines.length === 1) {
5290
+ this.insertChar(lines[0] ?? "");
5291
+ return;
5292
+ }
5293
+ const cur = this.currentLine();
5294
+ const before = cur.slice(0, this.col);
5295
+ const after = cur.slice(this.col);
5296
+ const first = lines[0] ?? "";
5297
+ const last = lines[lines.length - 1] ?? "";
5298
+ const middle = lines.slice(1, -1);
5299
+ this.setCurrentLine(before + first);
5300
+ const newRows = [...middle, last + after];
5301
+ this.buffer.splice(this.row + 1, 0, ...newRows);
5302
+ this.row += lines.length - 1;
5303
+ this.col = last.length;
5304
+ }
5305
+ insertNewline() {
5306
+ const line = this.currentLine();
5307
+ const before = line.slice(0, this.col);
5308
+ const after = line.slice(this.col);
5309
+ this.setCurrentLine(before);
5310
+ this.buffer.splice(this.row + 1, 0, after);
5311
+ this.row += 1;
5312
+ this.col = 0;
5313
+ }
5314
+ backspace() {
5315
+ if (this.col > 0) {
5316
+ const line = this.currentLine();
5317
+ this.setCurrentLine(line.slice(0, this.col - 1) + line.slice(this.col));
5318
+ this.col -= 1;
5319
+ return;
5320
+ }
5321
+ if (this.row === 0) {
5322
+ return;
5323
+ }
5324
+ const prev = this.buffer[this.row - 1] ?? "";
5325
+ const cur = this.currentLine();
5326
+ this.buffer.splice(this.row, 1);
5327
+ this.row -= 1;
5328
+ this.col = prev.length;
5329
+ this.buffer[this.row] = prev + cur;
5330
+ }
5331
+ deleteForward() {
5332
+ const line = this.currentLine();
5333
+ if (this.col < line.length) {
5334
+ this.setCurrentLine(line.slice(0, this.col) + line.slice(this.col + 1));
5335
+ return;
5336
+ }
5337
+ if (this.row < this.buffer.length - 1) {
5338
+ const next = this.buffer[this.row + 1] ?? "";
5339
+ this.buffer.splice(this.row + 1, 1);
5340
+ this.setCurrentLine(line + next);
5341
+ }
5342
+ }
5343
+ // ^U: kill from cursor to start of current line. At col 0 with a line
5344
+ // above:
5345
+ // - If the current line is empty, collapse it (kill just the
5346
+ // newline) so the cursor lands at the end of the previous line.
5347
+ // Don't slurp that line's contents.
5348
+ // - Otherwise, kill the previous line entirely + the joining
5349
+ // newline, so ^U from the start of a non-empty line walks up
5350
+ // line-by-line.
5351
+ // Single-line behavior is unchanged.
5352
+ killLine() {
5353
+ if (this.col > 0) {
5354
+ const line = this.currentLine();
5355
+ this.killBuffer = line.slice(0, this.col);
5356
+ this.setCurrentLine(line.slice(this.col));
5357
+ this.col = 0;
5358
+ return;
5359
+ }
5360
+ if (this.row === 0) {
5361
+ return;
5362
+ }
5363
+ if (this.currentLine().length === 0) {
5364
+ this.killBuffer = "\n";
5365
+ this.buffer.splice(this.row, 1);
5366
+ this.row -= 1;
5367
+ this.col = this.currentLine().length;
5368
+ return;
5369
+ }
5370
+ const prev = this.buffer[this.row - 1] ?? "";
5371
+ this.killBuffer = prev + "\n";
5372
+ this.buffer.splice(this.row - 1, 1);
5373
+ this.row -= 1;
5374
+ }
5375
+ // ^K: kill from cursor to end of current line. At end-of-line with a
5376
+ // line below:
5377
+ // - If the current line is empty, collapse it (kill just the
5378
+ // newline) so what was the next line takes its place. Don't slurp
5379
+ // that line's contents.
5380
+ // - Otherwise, kill the joining newline + the entire next line, so
5381
+ // ^K from the end of a non-empty line walks down line-by-line.
5382
+ // Single-line behavior is unchanged.
5383
+ killToEnd() {
5384
+ const line = this.currentLine();
5385
+ if (this.col < line.length) {
5386
+ this.killBuffer = line.slice(this.col);
5387
+ this.setCurrentLine(line.slice(0, this.col));
5388
+ return;
5389
+ }
5390
+ if (this.row >= this.buffer.length - 1) {
5391
+ return;
5392
+ }
5393
+ if (line.length === 0) {
5394
+ this.killBuffer = "\n";
5395
+ this.buffer.splice(this.row, 1);
5396
+ return;
5397
+ }
5398
+ const next = this.buffer[this.row + 1] ?? "";
5399
+ this.killBuffer = "\n" + next;
5400
+ this.buffer.splice(this.row + 1, 1);
5401
+ }
5402
+ killWord() {
5403
+ const line = this.currentLine();
5404
+ if (this.col === 0) {
5405
+ this.backspace();
5406
+ return;
5407
+ }
5408
+ let i = this.col;
5409
+ while (i > 0 && /\s/.test(line[i - 1] ?? "")) {
5410
+ i -= 1;
5411
+ }
5412
+ while (i > 0 && !/\s/.test(line[i - 1] ?? "")) {
5413
+ i -= 1;
5414
+ }
5415
+ const killed = line.slice(i, this.col);
5416
+ if (killed.length > 0) {
5417
+ this.killBuffer = killed;
5418
+ }
5419
+ this.setCurrentLine(line.slice(0, i) + line.slice(this.col));
5420
+ this.col = i;
5421
+ }
5422
+ yank() {
5423
+ if (this.killBuffer.length === 0) {
5424
+ return;
5425
+ }
5426
+ this.insertText(this.killBuffer);
5427
+ }
5428
+ moveLeft() {
5429
+ if (this.col > 0) {
5430
+ this.col -= 1;
5431
+ return;
5432
+ }
5433
+ if (this.row > 0) {
5434
+ this.row -= 1;
5435
+ this.col = this.currentLine().length;
5436
+ }
5437
+ }
5438
+ moveRight() {
5439
+ if (this.col < this.currentLine().length) {
5440
+ this.col += 1;
5441
+ return;
5442
+ }
5443
+ if (this.row < this.buffer.length - 1) {
5444
+ this.row += 1;
5445
+ this.col = 0;
5446
+ }
5447
+ }
5448
+ moveWordBackward() {
5449
+ if (this.col === 0) {
5450
+ if (this.row === 0) {
5451
+ return;
5452
+ }
5453
+ this.row -= 1;
5454
+ this.col = this.currentLine().length;
5455
+ return;
5456
+ }
5457
+ const line = this.currentLine();
5458
+ let i = this.col;
5459
+ while (i > 0 && /\s/.test(line[i - 1] ?? "")) {
5460
+ i -= 1;
5461
+ }
5462
+ while (i > 0 && !/\s/.test(line[i - 1] ?? "")) {
5463
+ i -= 1;
5464
+ }
5465
+ this.col = i;
5981
5466
  }
5982
- if (name === "ENTER" || name === "KP_ENTER") {
5983
- const candidate = buffer;
5984
- busy = true;
5985
- errorLine = null;
5986
- repaintInput();
5987
- void validateLocalCwd(candidate).then((result) => {
5988
- busy = false;
5989
- if (result.ok) {
5990
- finish({ kind: "ok", path: result.path });
5467
+ moveWordForward() {
5468
+ const line = this.currentLine();
5469
+ if (this.col >= line.length) {
5470
+ if (this.row >= this.buffer.length - 1) {
5991
5471
  return;
5992
5472
  }
5993
- errorLine = result.reason;
5994
- repaintInput();
5995
- });
5996
- return;
5997
- }
5998
- if (name === "ESCAPE") {
5999
- finish({ kind: "back" });
6000
- return;
6001
- }
6002
- if (name === "CTRL_C" || name === "CTRL_D") {
6003
- finish({ kind: "cancel" });
6004
- return;
6005
- }
6006
- if (name === "BACKSPACE") {
6007
- if (buffer.length > 0) {
6008
- buffer = buffer.slice(0, -1);
6009
- errorLine = null;
6010
- repaintInput();
5473
+ this.row += 1;
5474
+ this.col = 0;
5475
+ return;
6011
5476
  }
6012
- return;
5477
+ let i = this.col;
5478
+ while (i < line.length && /\s/.test(line[i] ?? "")) {
5479
+ i += 1;
5480
+ }
5481
+ while (i < line.length && !/\s/.test(line[i] ?? "")) {
5482
+ i += 1;
5483
+ }
5484
+ this.col = i;
6013
5485
  }
6014
- if (name === "CTRL_U") {
6015
- buffer = "";
6016
- errorLine = null;
6017
- repaintInput();
6018
- return;
5486
+ // Up walks the navigation stack from newest to oldest: pending queue
5487
+ // items first (so the user can edit something they just enqueued),
5488
+ // then prompt history. Cursor movement within a multi-line buffer
5489
+ // takes priority when not already navigating.
5490
+ handleUp() {
5491
+ if (this.row > 0) {
5492
+ this.row -= 1;
5493
+ this.col = Math.min(this.col, this.currentLine().length);
5494
+ return [];
5495
+ }
5496
+ if (this.queueIndex === -1 && this.historyIndex === -1) {
5497
+ if (this.queue.length === 0 && this.history.length === 0) {
5498
+ return [];
5499
+ }
5500
+ this.savedDraft = {
5501
+ buffer: [...this.buffer],
5502
+ row: this.row,
5503
+ col: this.col
5504
+ };
5505
+ this.savedAttachments = [...this.attachments];
5506
+ this.attachments = [];
5507
+ if (this.queue.length > 0) {
5508
+ this.queueIndex = this.queue.length - 1;
5509
+ this.loadEntry(this.queue[this.queueIndex] ?? "");
5510
+ } else {
5511
+ this.historyIndex = this.history.length - 1;
5512
+ this.loadEntry(this.history[this.historyIndex] ?? "");
5513
+ }
5514
+ return [];
5515
+ }
5516
+ if (this.queueIndex >= 0) {
5517
+ if (this.queueIndex > 0) {
5518
+ this.queueIndex -= 1;
5519
+ this.loadEntry(this.queue[this.queueIndex] ?? "");
5520
+ return [];
5521
+ }
5522
+ if (this.history.length === 0) {
5523
+ return [];
5524
+ }
5525
+ this.queueIndex = -1;
5526
+ this.historyIndex = this.history.length - 1;
5527
+ this.loadEntry(this.history[this.historyIndex] ?? "");
5528
+ return [];
5529
+ }
5530
+ if (this.historyIndex > 0) {
5531
+ this.historyIndex -= 1;
5532
+ this.loadEntry(this.history[this.historyIndex] ?? "");
5533
+ }
5534
+ return [];
6019
5535
  }
6020
- if (name === "CTRL_W") {
6021
- const trimmedRight = buffer.replace(/[/\s]+$/, "");
6022
- const lastSep = Math.max(
6023
- trimmedRight.lastIndexOf("/"),
6024
- trimmedRight.lastIndexOf(" ")
6025
- );
6026
- buffer = lastSep >= 0 ? trimmedRight.slice(0, lastSep + 1) : "";
6027
- errorLine = null;
6028
- repaintInput();
6029
- return;
5536
+ // Down reverses the Up walk: history (older newer), then queue
5537
+ // (oldest newest), then restore the original draft. Within a
5538
+ // multi-line buffer, plain cursor movement still wins when no
5539
+ // navigation is in progress.
5540
+ handleDown() {
5541
+ if (this.row < this.buffer.length - 1 && this.historyIndex === -1 && this.queueIndex === -1) {
5542
+ this.row += 1;
5543
+ this.col = Math.min(this.col, this.currentLine().length);
5544
+ return [];
5545
+ }
5546
+ if (this.historyIndex >= 0) {
5547
+ if (this.historyIndex < this.history.length - 1) {
5548
+ this.historyIndex += 1;
5549
+ this.loadEntry(this.history[this.historyIndex] ?? "");
5550
+ return [];
5551
+ }
5552
+ this.historyIndex = -1;
5553
+ if (this.queue.length > 0) {
5554
+ this.queueIndex = 0;
5555
+ this.loadEntry(this.queue[this.queueIndex] ?? "");
5556
+ return [];
5557
+ }
5558
+ this.restoreDraft();
5559
+ return [];
5560
+ }
5561
+ if (this.queueIndex >= 0) {
5562
+ if (this.queueIndex < this.queue.length - 1) {
5563
+ this.queueIndex += 1;
5564
+ this.loadEntry(this.queue[this.queueIndex] ?? "");
5565
+ return [];
5566
+ }
5567
+ this.queueIndex = -1;
5568
+ this.restoreDraft();
5569
+ return [];
5570
+ }
5571
+ return [];
6030
5572
  }
6031
- if (data?.isCharacter) {
6032
- buffer += name;
6033
- errorLine = null;
6034
- repaintInput();
6035
- return;
5573
+ restoreDraft() {
5574
+ if (this.savedDraft) {
5575
+ this.buffer = [...this.savedDraft.buffer];
5576
+ this.row = this.savedDraft.row;
5577
+ this.col = this.savedDraft.col;
5578
+ this.savedDraft = null;
5579
+ this.attachments = this.savedAttachments ?? [];
5580
+ this.savedAttachments = null;
5581
+ } else {
5582
+ this.clearBuffer();
5583
+ }
6036
5584
  }
6037
- };
6038
- term.grabInput({});
6039
- term.on("key", onKey);
6040
- term.on("resize", onResize);
6041
- });
6042
- }
6043
- function truncate(s, max) {
6044
- if (max <= 1) {
6045
- return "";
6046
- }
6047
- if (s.length <= max) {
6048
- return s;
6049
- }
6050
- return s.slice(0, Math.max(0, max - 1)) + "\u2026";
6051
- }
6052
- function truncateLeft(s, max) {
6053
- if (max <= 1) {
6054
- return "";
6055
- }
6056
- if (s.length <= max) {
6057
- return s;
6058
- }
6059
- return "\u2026" + s.slice(s.length - (max - 1));
6060
- }
6061
- var init_import_cwd_prompt = __esm({
6062
- "src/tui/import-cwd-prompt.ts"() {
6063
- "use strict";
6064
- init_paths();
6065
- init_session();
6066
- init_cwd();
6067
- init_prompt_utils();
6068
- }
6069
- });
6070
-
6071
- // src/tui/import-action-prompt.ts
6072
- function actionPromptStep(selected, key, choices = ACTION_CHOICES) {
6073
- if (key.kind === "cancel") {
6074
- return { kind: "cancel" };
6075
- }
6076
- if (key.kind === "back") {
6077
- return { kind: "back" };
6078
- }
6079
- if (key.kind === "enter") {
6080
- const choice = choices[selected];
6081
- if (!choice) {
6082
- return { kind: "back" };
6083
- }
6084
- return { kind: "resolve", action: choice.key };
6085
- }
6086
- if (key.kind === "up") {
6087
- return {
6088
- kind: "continue",
6089
- selected: Math.max(0, selected - 1)
6090
- };
6091
- }
6092
- if (key.kind === "down") {
6093
- return {
6094
- kind: "continue",
6095
- selected: Math.min(choices.length - 1, selected + 1)
6096
- };
6097
- }
6098
- if (key.kind === "char") {
6099
- const lower = key.ch.toLowerCase();
6100
- if (lower === "n") {
6101
- return {
6102
- kind: "continue",
6103
- selected: Math.min(choices.length - 1, selected + 1)
6104
- };
6105
- }
6106
- if (lower === "p") {
6107
- return {
6108
- kind: "continue",
6109
- selected: Math.max(0, selected - 1)
6110
- };
6111
- }
6112
- const idx = choices.findIndex((c) => c.hotkey.toLowerCase() === lower);
6113
- if (idx >= 0) {
6114
- const choice = choices[idx];
6115
- if (choice) {
6116
- return { kind: "resolve", action: choice.key };
5585
+ // Engage reverse-incremental search over prompt history. Uses the
5586
+ // current buffer text as the search query. With an empty buffer we
5587
+ // enter search mode in an "empty query, no match shown" state — the
5588
+ // banner indicator lights up, and as the user types we extend the
5589
+ // query and load top matches. We deliberately do NOT auto-load the
5590
+ // most recent entry on an empty ^R (that's a surprise — Up-arrow
5591
+ // already walks history if that's what they wanted). With a
5592
+ // non-empty query that has no history match, escalate straight to
5593
+ // scrollback search so the typed term searches session output.
5594
+ startHistorySearch() {
5595
+ const query = this.bufferText().toLowerCase();
5596
+ if (query.length === 0) {
5597
+ this.historySearch = {
5598
+ query: "",
5599
+ matchIndices: [],
5600
+ cursor: 0,
5601
+ savedDraft: {
5602
+ buffer: [...this.buffer],
5603
+ row: this.row,
5604
+ col: this.col
5605
+ }
5606
+ };
5607
+ return [];
5608
+ }
5609
+ const matchIndices = this.findHistoryMatches(query);
5610
+ if (matchIndices.length === 0) {
5611
+ return [{ type: "escalate-search", query }];
5612
+ }
5613
+ this.historySearch = {
5614
+ query,
5615
+ matchIndices,
5616
+ cursor: 0,
5617
+ savedDraft: {
5618
+ buffer: [...this.buffer],
5619
+ row: this.row,
5620
+ col: this.col
5621
+ }
5622
+ };
5623
+ this.loadEntry(this.history[matchIndices[0]] ?? "");
5624
+ return [];
6117
5625
  }
6118
- }
6119
- }
6120
- return { kind: "continue", selected };
6121
- }
6122
- async function promptForImportAction(term, session) {
6123
- resetTerminalModes();
6124
- const shortId2 = stripHydraSessionPrefix(session.sessionId);
6125
- const fromMachine = session.importedFromMachine ?? "another machine";
6126
- const originalCwd = shortenHomePath(session.cwd);
6127
- let selected = 0;
6128
- const render = () => {
6129
- const choiceRows = ACTION_CHOICES.length * 2;
6130
- const contentHeight = 7 + choiceRows + 2;
6131
- const layout = drawBox(term, {
6132
- contentHeight,
6133
- title: "Imported session"
6134
- });
6135
- const innerW = layout.contentW;
6136
- const headerRows = [
6137
- { label: "session: ", value: shortId2 },
6138
- { label: "from: ", value: fromMachine },
6139
- { label: "cwd: ", value: originalCwd }
6140
- ];
6141
- let row = 0;
6142
- for (const hr of headerRows) {
6143
- term.moveTo(layout.contentX, layout.contentY + row);
6144
- term.dim.noFormat(` ${hr.label}`);
6145
- term.noFormat(truncate2(hr.value, innerW - hr.label.length - 2));
6146
- row++;
6147
- }
6148
- row++;
6149
- term.moveTo(layout.contentX, layout.contentY + row);
6150
- term.noFormat(" What do you want to do?");
6151
- row += 2;
6152
- for (let i = 0; i < ACTION_CHOICES.length; i++) {
6153
- const choice = ACTION_CHOICES[i];
6154
- if (!choice) {
6155
- continue;
5626
+ // ^R advance. At the oldest match with a non-empty query, falls
5627
+ // through to scrollback search (same escalate path as a never-
5628
+ // matched startHistorySearch). With an empty query at the oldest
5629
+ // match (i.e. the user walked all history with no filter), advance
5630
+ // is a no-op so the buffer stays on the oldest entry.
5631
+ advanceHistorySearch() {
5632
+ if (this.historySearch === null) {
5633
+ return [];
5634
+ }
5635
+ const search = this.historySearch;
5636
+ const atOldest = search.cursor >= search.matchIndices.length - 1;
5637
+ if (atOldest) {
5638
+ if (search.query.length === 0) {
5639
+ return [];
5640
+ }
5641
+ const query = search.query;
5642
+ const draft = search.savedDraft;
5643
+ this.historySearch = null;
5644
+ this.buffer = [...draft.buffer];
5645
+ this.row = draft.row;
5646
+ this.col = draft.col;
5647
+ return [{ type: "escalate-search", query }];
5648
+ }
5649
+ search.cursor += 1;
5650
+ const idx = search.matchIndices[search.cursor];
5651
+ this.loadEntry(this.history[idx] ?? "");
5652
+ return [];
6156
5653
  }
6157
- const pointer = i === selected ? "\u276F" : " ";
6158
- const label = ` ${pointer} ${choice.label}`;
6159
- term.moveTo(layout.contentX, layout.contentY + row);
6160
- if (i === selected) {
6161
- term.brightWhite.bgBlue.noFormat(padRight(label, innerW));
6162
- } else {
6163
- term.noFormat(label);
5654
+ // ^S retreat walk toward newer matches. No-op at the newest match
5655
+ // (no wrap, mirroring ^R no-wrap at the oldest).
5656
+ retreatHistorySearch() {
5657
+ if (this.historySearch === null) {
5658
+ return;
5659
+ }
5660
+ if (this.historySearch.cursor === 0) {
5661
+ return;
5662
+ }
5663
+ this.historySearch.cursor -= 1;
5664
+ const idx = this.historySearch.matchIndices[this.historySearch.cursor];
5665
+ this.loadEntry(this.history[idx] ?? "");
6164
5666
  }
6165
- row++;
6166
- term.moveTo(layout.contentX, layout.contentY + row);
6167
- term.dim.noFormat(` ${choice.description}`);
6168
- row++;
6169
- }
6170
- row++;
6171
- term.moveTo(layout.contentX, layout.contentY + row);
6172
- term.dim.noFormat(" \u2191/\u2193 navigate \xB7 Enter select \xB7 r/v jump \xB7 Esc back");
6173
- return layout;
6174
- };
6175
- render();
6176
- term.hideCursor();
6177
- return await new Promise((resolve6) => {
6178
- let resolved = false;
6179
- const cleanup = () => {
6180
- if (resolved) {
6181
- return;
5667
+ // Backspace / typing within search mode mutates the query and
5668
+ // re-searches. When the new query is empty, restore the saved
5669
+ // draft buffer (typically empty) and stay in search mode — the
5670
+ // user can keep typing. When the new query has matches, load the
5671
+ // top one. When the new query has no matches, escalate to scrollback
5672
+ // search so the typed term applies there instead.
5673
+ mutateHistorySearchQuery(newQuery) {
5674
+ if (this.historySearch === null) {
5675
+ return [];
5676
+ }
5677
+ if (newQuery.length === 0) {
5678
+ this.historySearch.query = "";
5679
+ this.historySearch.matchIndices = [];
5680
+ this.historySearch.cursor = 0;
5681
+ const draft = this.historySearch.savedDraft;
5682
+ this.buffer = [...draft.buffer];
5683
+ this.row = draft.row;
5684
+ this.col = draft.col;
5685
+ return [];
5686
+ }
5687
+ const matchIndices = this.findHistoryMatches(newQuery);
5688
+ if (matchIndices.length === 0) {
5689
+ const draft = this.historySearch.savedDraft;
5690
+ this.historySearch = null;
5691
+ this.buffer = [...draft.buffer];
5692
+ this.row = draft.row;
5693
+ this.col = draft.col;
5694
+ return [{ type: "escalate-search", query: newQuery }];
5695
+ }
5696
+ this.historySearch.query = newQuery;
5697
+ this.historySearch.matchIndices = matchIndices;
5698
+ this.historySearch.cursor = 0;
5699
+ this.loadEntry(this.history[matchIndices[0]] ?? "");
5700
+ return [];
6182
5701
  }
6183
- resolved = true;
6184
- term.off("key", onKey);
6185
- term.off("resize", onResize);
6186
- term.grabInput(false);
6187
- term.hideCursor(false);
6188
- term.moveTo(1, 1).eraseDisplayBelow();
6189
- };
6190
- const finish = (value) => {
6191
- cleanup();
6192
- resolve6(value);
6193
- };
6194
- const onResize = () => {
6195
- if (resolved) {
6196
- return;
5702
+ findHistoryMatches(query) {
5703
+ const out = [];
5704
+ for (let i = this.history.length - 1; i >= 0; i--) {
5705
+ const entry = this.history[i] ?? "";
5706
+ if (query.length === 0 || entry.toLowerCase().includes(query)) {
5707
+ out.push(i);
5708
+ }
5709
+ }
5710
+ return out;
6197
5711
  }
6198
- render();
6199
- };
6200
- const onKey = (name, _matches, data) => {
6201
- const input = mapKey(name, data);
6202
- if (!input) {
6203
- return;
5712
+ cancelHistorySearch() {
5713
+ if (this.historySearch === null) {
5714
+ return;
5715
+ }
5716
+ const draft = this.historySearch.savedDraft;
5717
+ this.historySearch = null;
5718
+ this.buffer = [...draft.buffer];
5719
+ this.row = draft.row;
5720
+ this.col = draft.col;
6204
5721
  }
6205
- const step = actionPromptStep(selected, input);
6206
- if (step.kind === "cancel") {
6207
- finish("cancel");
6208
- return;
5722
+ loadEntry(text) {
5723
+ this.buffer = text.split("\n");
5724
+ if (this.buffer.length === 0) {
5725
+ this.buffer = [""];
5726
+ }
5727
+ this.row = this.buffer.length - 1;
5728
+ this.col = (this.buffer[this.row] ?? "").length;
6209
5729
  }
6210
- if (step.kind === "back") {
6211
- finish("back");
6212
- return;
5730
+ send() {
5731
+ const text = this.bufferText();
5732
+ if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
5733
+ const index = this.queueIndex;
5734
+ const attachments2 = [...this.attachments];
5735
+ this.clearBuffer();
5736
+ if (text.trim().length === 0) {
5737
+ return [{ type: "queue-remove", index }];
5738
+ }
5739
+ return [{ type: "queue-edit", index, text, attachments: attachments2 }];
5740
+ }
5741
+ if (text.trim().length === 0 && this.attachments.length === 0) {
5742
+ return [];
5743
+ }
5744
+ const planMode = this.planMode;
5745
+ const attachments = [...this.attachments];
5746
+ this.clearBuffer();
5747
+ return [{ type: "send", text, planMode, attachments }];
5748
+ }
5749
+ // Shift+Enter: amend the in-flight turn. Editing a queued slot
5750
+ // delegates to the existing queue-edit / queue-remove path — Shift+Enter
5751
+ // there has no special meaning since the entry is already queued (not
5752
+ // running). With an empty draft and no attachments we emit nothing
5753
+ // (no-op). Otherwise emit an "amend" effect; the app decides whether
5754
+ // to route through amend_prompt or fall through to a regular send.
5755
+ amend() {
5756
+ const text = this.bufferText();
5757
+ if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
5758
+ const index = this.queueIndex;
5759
+ const attachments2 = [...this.attachments];
5760
+ this.clearBuffer();
5761
+ if (text.trim().length === 0) {
5762
+ return [{ type: "queue-remove", index }];
5763
+ }
5764
+ return [{ type: "queue-edit", index, text, attachments: attachments2 }];
5765
+ }
5766
+ if (text.trim().length === 0 && this.attachments.length === 0) {
5767
+ return [];
5768
+ }
5769
+ const planMode = this.planMode;
5770
+ const attachments = [...this.attachments];
5771
+ this.clearBuffer();
5772
+ return [{ type: "amend", text, planMode, attachments }];
5773
+ }
5774
+ // Home: jump to the very start of the prompt buffer. If we're already
5775
+ // there, fall through to scrolling the scrollback to its top.
5776
+ handleHome() {
5777
+ if (this.row !== 0 || this.col !== 0) {
5778
+ this.row = 0;
5779
+ this.col = 0;
5780
+ return [];
5781
+ }
5782
+ return [{ type: "scroll-to-top" }];
6213
5783
  }
6214
- if (step.kind === "resolve") {
6215
- finish(step.action);
6216
- return;
5784
+ // End: jump to the end of the last line of the prompt buffer. Already
5785
+ // there → scroll the scrollback to the bottom (newest).
5786
+ handleEnd() {
5787
+ const lastRow = this.buffer.length - 1;
5788
+ const lastCol = (this.buffer[lastRow] ?? "").length;
5789
+ if (this.row !== lastRow || this.col !== lastCol) {
5790
+ this.row = lastRow;
5791
+ this.col = lastCol;
5792
+ return [];
5793
+ }
5794
+ return [{ type: "scroll-to-bottom" }];
6217
5795
  }
6218
- if (step.selected !== selected) {
6219
- selected = step.selected;
6220
- render();
5796
+ handleCtrlC() {
5797
+ if (this.queueIndex >= 0) {
5798
+ const index = this.queueIndex;
5799
+ this.queueIndex = -1;
5800
+ this.restoreDraft();
5801
+ return [{ type: "queue-remove", index }];
5802
+ }
5803
+ if (!this.bufferIsEmpty() || this.attachments.length > 0) {
5804
+ this.buffer = [""];
5805
+ this.row = 0;
5806
+ this.col = 0;
5807
+ this.attachments = [];
5808
+ this.historyIndex = -1;
5809
+ this.savedDraft = null;
5810
+ this.savedAttachments = null;
5811
+ return [];
5812
+ }
5813
+ if (this.turnRunning) {
5814
+ return [{ type: "cancel" }];
5815
+ }
5816
+ return [{ type: "exit" }];
6221
5817
  }
6222
5818
  };
6223
- term.grabInput({});
6224
- term.on("key", onKey);
6225
- term.on("resize", onResize);
6226
- });
6227
- }
6228
- function mapKey(name, data) {
6229
- if (name === "UP") {
6230
- return { kind: "up" };
6231
- }
6232
- if (name === "DOWN") {
6233
- return { kind: "down" };
6234
- }
6235
- if (name === "ENTER" || name === "KP_ENTER") {
6236
- return { kind: "enter" };
6237
- }
6238
- if (name === "ESCAPE") {
6239
- return { kind: "back" };
6240
- }
6241
- if (name === "CTRL_C" || name === "CTRL_D") {
6242
- return { kind: "cancel" };
6243
- }
6244
- if (data?.isCharacter) {
6245
- return { kind: "char", ch: name };
6246
- }
6247
- return null;
6248
- }
6249
- function truncate2(s, max) {
6250
- if (max <= 1) {
6251
- return "";
6252
- }
6253
- if (s.length <= max) {
6254
- return s;
6255
- }
6256
- return s.slice(0, Math.max(0, max - 1)) + "\u2026";
6257
- }
6258
- function padRight(s, w) {
6259
- if (s.length >= w) {
6260
- return s.slice(0, w);
6261
- }
6262
- return s + " ".repeat(w - s.length);
6263
- }
6264
- var ACTION_CHOICES;
6265
- var init_import_action_prompt = __esm({
6266
- "src/tui/import-action-prompt.ts"() {
6267
- "use strict";
6268
- init_paths();
6269
- init_session();
6270
- init_prompt_utils();
6271
- ACTION_CHOICES = [
6272
- {
6273
- key: "run-local",
6274
- label: "Run locally",
6275
- hotkey: "r",
6276
- description: "spawn the agent on this machine with a local cwd"
6277
- },
6278
- {
6279
- key: "view",
6280
- label: "View transcript",
6281
- hotkey: "v",
6282
- description: "open read-only, no agent spawn"
6283
- }
6284
- ];
6285
5819
  }
6286
5820
  });
6287
5821
 
6288
5822
  // src/tui/attachments.ts
6289
- import path14 from "path";
5823
+ import path13 from "path";
6290
5824
  function mimeFromExtension(p) {
6291
- return EXTENSION_TO_MIME[path14.extname(p).toLowerCase()] ?? null;
5825
+ return EXTENSION_TO_MIME[path13.extname(p).toLowerCase()] ?? null;
6292
5826
  }
6293
5827
  function isSupportedImagePath(p) {
6294
5828
  return mimeFromExtension(p) !== null;
@@ -6748,7 +6282,7 @@ function graphemeSegments(text) {
6748
6282
  }
6749
6283
  return out;
6750
6284
  }
6751
- function truncate3(text, max, opts = {}) {
6285
+ function truncate(text, max, opts = {}) {
6752
6286
  if (max <= 0) {
6753
6287
  return "";
6754
6288
  }
@@ -8398,9 +7932,9 @@ uncaught: ${err.stack ?? err.message}
8398
7932
  titleRoom = 0;
8399
7933
  cwdRoom = variableRoom;
8400
7934
  }
8401
- this.term.yellow(sid)(" \xB7 ").cyan.noFormat(agentCell)(" \xB7 ").dim.noFormat(truncate3(cwdDisplay, cwdRoom));
7935
+ this.term.yellow(sid)(" \xB7 ").cyan.noFormat(agentCell)(" \xB7 ").dim.noFormat(truncate(cwdDisplay, cwdRoom));
8402
7936
  if (title) {
8403
- this.term(" \xB7 ").bold.noFormat(truncate3(title, titleRoom));
7937
+ this.term(" \xB7 ").bold.noFormat(truncate(title, titleRoom));
8404
7938
  }
8405
7939
  if (usage) {
8406
7940
  const col = Math.max(1, w - usage.length + 1);
@@ -8511,7 +8045,7 @@ uncaught: ${err.stack ?? err.message}
8511
8045
  const namePadded = item.name.padEnd(nameWidth);
8512
8046
  const desc = item.description ?? "";
8513
8047
  const remaining = w - namePadded.length - 4;
8514
- const truncated = remaining > 0 ? truncate3(desc, remaining) : "";
8048
+ const truncated = remaining > 0 ? truncate(desc, remaining) : "";
8515
8049
  this.term(" ").brightCyan(namePadded);
8516
8050
  if (truncated.length > 0) {
8517
8051
  this.term(" ").dim(truncated);
@@ -8588,7 +8122,7 @@ uncaught: ${err.stack ?? err.message}
8588
8122
  const text = this.queuedTexts[i];
8589
8123
  const isLast = i === rows - 1 && this.queuedTexts.length > MAX_QUEUED_ROWS;
8590
8124
  const overflow = this.queuedTexts.length - MAX_QUEUED_ROWS;
8591
- const summary = text === void 0 ? "" : isLast ? `+ ${overflow + 1} more queued` : truncate3(firstLine2(text), w - 4);
8125
+ const summary = text === void 0 ? "" : isLast ? `+ ${overflow + 1} more queued` : truncate(firstLine2(text), w - 4);
8592
8126
  const editing = !isLast && i === editingIndex;
8593
8127
  const sig = text === void 0 ? `queued|${w}|empty` : `queued|${w}|${editing ? "edit" : isLast ? "ovf" : "row"}|${summary}`;
8594
8128
  this.paintRow(row, sig, () => {
@@ -8665,10 +8199,10 @@ uncaught: ${err.stack ?? err.message}
8665
8199
  const w = this.term.width;
8666
8200
  const top = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
8667
8201
  this.paintRow(top, `confirm|q|${w}|${spec.question}`, () => {
8668
- this.term.brightYellow(` ? ${truncate3(spec.question, w - 4)}`);
8202
+ this.term.brightYellow(` ? ${truncate(spec.question, w - 4)}`);
8669
8203
  });
8670
8204
  this.paintRow(top + 1, `confirm|h|${w}|${spec.hint}`, () => {
8671
- this.term.dim(` ${truncate3(spec.hint, w - 2)}`);
8205
+ this.term.dim(` ${truncate(spec.hint, w - 2)}`);
8672
8206
  });
8673
8207
  }
8674
8208
  drawHelpPrompt() {
@@ -8688,7 +8222,7 @@ uncaught: ${err.stack ?? err.message}
8688
8222
  row += 1;
8689
8223
  };
8690
8224
  writeRow(`help|t|${w}|${spec.title}`, () => {
8691
- this.term.brightYellow(` \u2753 ${truncate3(spec.title, w - 5)}`);
8225
+ this.term.brightYellow(` \u2753 ${truncate(spec.title, w - 5)}`);
8692
8226
  });
8693
8227
  const keysWidth = Math.min(
8694
8228
  24,
@@ -8710,11 +8244,11 @@ uncaught: ${err.stack ?? err.message}
8710
8244
  writeRow(`help|e|${w}|${keys}|${desc}`, () => {
8711
8245
  this.term(" ");
8712
8246
  this.term.brightCyan.noFormat(paddedKeys);
8713
- this.term.noFormat(` ${truncate3(desc, w - 2 - keysWidth - 1)}`);
8247
+ this.term.noFormat(` ${truncate(desc, w - 2 - keysWidth - 1)}`);
8714
8248
  });
8715
8249
  }
8716
8250
  writeRow(`help|hint|${w}|${spec.hint}`, () => {
8717
- this.term.dim(` ${truncate3(spec.hint, w - 2)}`);
8251
+ this.term.dim(` ${truncate(spec.hint, w - 2)}`);
8718
8252
  });
8719
8253
  }
8720
8254
  helpRows() {
@@ -8740,7 +8274,7 @@ uncaught: ${err.stack ?? err.message}
8740
8274
  row += 1;
8741
8275
  };
8742
8276
  writeRow(`perm|t|${w}|${spec.title}`, () => {
8743
- this.term.brightYellow(` \u{1F512} ${truncate3(spec.title, w - 5)}`);
8277
+ this.term.brightYellow(` \u{1F512} ${truncate(spec.title, w - 5)}`);
8744
8278
  });
8745
8279
  writeRow(`perm|sub|${w}`, () => {
8746
8280
  this.term.dim(" This action requires approval");
@@ -8758,7 +8292,7 @@ uncaught: ${err.stack ?? err.message}
8758
8292
  }
8759
8293
  const isSel = i === spec.selectedIndex;
8760
8294
  const marker = isSel ? "\u276F" : " ";
8761
- const body = ` ${marker} ${i + 1}. ${truncate3(opt.label, w - 8)}`;
8295
+ const body = ` ${marker} ${i + 1}. ${truncate(opt.label, w - 8)}`;
8762
8296
  writeRow(`perm|o|${w}|${i}|${isSel ? "1" : "0"}|${opt.label}`, () => {
8763
8297
  if (isSel) {
8764
8298
  this.term.brightCyan(body);
@@ -8797,1075 +8331,1726 @@ uncaught: ${err.stack ?? err.message}
8797
8331
  } else {
8798
8332
  this.term.brightGreen(`${dot} ${this.banner.status}`);
8799
8333
  }
8800
- if (this.banner.queued > 0) {
8801
- this.term(" \xB7 ").brightYellow(`${this.banner.queued} queued`);
8334
+ if (this.banner.queued > 0) {
8335
+ this.term(" \xB7 ").brightYellow(`${this.banner.queued} queued`);
8336
+ }
8337
+ if (this.scrollOffset > 0) {
8338
+ this.term(" \xB7 ").brightCyan(`\u2191 ${this.scrollOffset}`);
8339
+ }
8340
+ const hint = this.banner.currentMode ? this.banner.hint.replace(
8341
+ "\u21E7\u21E5 mode",
8342
+ `\u21E7\u21E5 mode: ${this.banner.currentMode}`
8343
+ ) : this.banner.hint;
8344
+ this.term(" \xB7 ").dim(hint);
8345
+ if (right) {
8346
+ const visibleWidth = stringWidth(right.text);
8347
+ const col = Math.max(1, w - visibleWidth + 1);
8348
+ this.term.moveTo(col, row).eraseLineAfter();
8349
+ if (right.kind === "search") {
8350
+ this.term.brightCyan.noFormat(right.text);
8351
+ } else {
8352
+ this.term.brightYellow.noFormat(right.text);
8353
+ }
8354
+ }
8355
+ });
8356
+ }
8357
+ placeCursor() {
8358
+ if (!this.started) {
8359
+ return;
8360
+ }
8361
+ if (this.permissionPrompt) {
8362
+ const rows = this.permissionRows();
8363
+ const top2 = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
8364
+ const optionRow = top2 + 3 + this.permissionPrompt.selectedIndex;
8365
+ const lastUsableRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
8366
+ this.term.moveTo(2, Math.min(optionRow, lastUsableRow));
8367
+ return;
8368
+ }
8369
+ if (this.confirmPrompt) {
8370
+ const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
8371
+ this.term.moveTo(2, top2);
8372
+ return;
8373
+ }
8374
+ if (this.helpPrompt) {
8375
+ const rows = this.helpRows();
8376
+ const top2 = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
8377
+ this.term.moveTo(2, top2);
8378
+ return;
8379
+ }
8380
+ if (this.scrollbackSearch) {
8381
+ this.term.hideCursor(true);
8382
+ return;
8383
+ }
8384
+ if (this.readonly) {
8385
+ this.term.hideCursor(true);
8386
+ return;
8387
+ }
8388
+ this.term.hideCursor(false);
8389
+ const w = this.term.width;
8390
+ const room = Math.max(1, w - 2);
8391
+ const state = this.dispatcher.state();
8392
+ const visualRows = computePromptVisualRows(state.buffer, room);
8393
+ const layout = computePromptLayout(visualRows, state, MAX_PROMPT_ROWS);
8394
+ const top = this.term.height - layout.rendered - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
8395
+ const row = top + Math.max(0, layout.cursorVisualRow - layout.windowStart);
8396
+ const col = layout.cursorVisualCol + 3;
8397
+ const lastPromptRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
8398
+ this.term.moveTo(
8399
+ Math.min(col, this.term.width),
8400
+ Math.min(row, lastPromptRow)
8401
+ );
8402
+ }
8403
+ promptRows() {
8404
+ if (this.permissionPrompt) {
8405
+ return this.permissionRows();
8406
+ }
8407
+ if (this.confirmPrompt) {
8408
+ return CONFIRM_PROMPT_ROWS;
8409
+ }
8410
+ if (this.helpPrompt) {
8411
+ return this.helpRows();
8412
+ }
8413
+ if (this.readonly) {
8414
+ return 0;
8415
+ }
8416
+ const w = this.term.width;
8417
+ const room = Math.max(1, w - 2);
8418
+ const state = this.dispatcher.state();
8419
+ const visualRows = computePromptVisualRows(state.buffer, room);
8420
+ return Math.min(MAX_PROMPT_ROWS, Math.max(1, visualRows.length));
8421
+ }
8422
+ permissionRows() {
8423
+ if (!this.permissionPrompt) {
8424
+ return 0;
8425
+ }
8426
+ return Math.min(
8427
+ MAX_PERMISSION_ROWS,
8428
+ 4 + this.permissionPrompt.options.length
8429
+ );
8430
+ }
8431
+ // Walk this.lines from the tail, accumulating wrapped rows via the
8432
+ // wrap cache, until we have at least `needed` rows or run out. Returns
8433
+ // the collected rows in original (top-down) order plus an `exhausted`
8434
+ // flag that's true iff we reached the head of this.lines. The hot path
8435
+ // (drawScrollback) only ever asks for `visibleRows + scrollOffset`
8436
+ // rows, so a 10k-line scrollback costs ~50 cache hits per repaint
8437
+ // instead of 10k. With `needed = Infinity` this walks everything and
8438
+ // doubles as a total-row counter for maxScrollOffset.
8439
+ wrapTail(width, needed) {
8440
+ if (width <= 4) {
8441
+ const take = Math.min(needed, this.lines.length);
8442
+ return {
8443
+ rows: this.lines.slice(this.lines.length - take),
8444
+ exhausted: needed >= this.lines.length
8445
+ };
8446
+ }
8447
+ if (this.wrapCacheWidth !== width) {
8448
+ this.wrapCache.clear();
8449
+ this.wrapCacheWidth = width;
8450
+ }
8451
+ if (needed <= 0 || this.lines.length === 0) {
8452
+ return { rows: [], exhausted: true };
8453
+ }
8454
+ const batches = [];
8455
+ let total = 0;
8456
+ let stoppedAt = 0;
8457
+ for (let i = this.lines.length - 1; i >= 0; i--) {
8458
+ const wrapped = this.wrapOne(this.lines[i], width);
8459
+ batches.push(wrapped);
8460
+ total += wrapped.length;
8461
+ stoppedAt = i;
8462
+ if (total >= needed) {
8463
+ break;
8464
+ }
8465
+ }
8466
+ const rows = [];
8467
+ for (let i = batches.length - 1; i >= 0; i--) {
8468
+ rows.push(...batches[i]);
8469
+ }
8470
+ return { rows, exhausted: stoppedAt === 0 };
8471
+ }
8472
+ wrapOne(line, width) {
8473
+ const id = this.lineIds.get(line);
8474
+ if (id !== void 0) {
8475
+ const cached2 = this.wrapCache.get(id);
8476
+ if (cached2) {
8477
+ return cached2;
8478
+ }
8479
+ }
8480
+ const prefix = line.prefix ?? "";
8481
+ const room = Math.max(1, width - prefix.length);
8482
+ const stripMarkup = line.bodyStyle === "agent";
8483
+ const chunks = line.ansi ? wrapAnsiBody(line.body, room) : wrap(line.body, room, { stripMarkup });
8484
+ const wrapped = [];
8485
+ let scanPos = 0;
8486
+ for (let i = 0; i < chunks.length; i++) {
8487
+ const chunk = chunks[i] ?? "";
8488
+ const wrappedLine = {
8489
+ prefix: i === 0 ? line.prefix : " ".repeat(prefix.length),
8490
+ body: chunk
8491
+ };
8492
+ if (line.prefixStyle !== void 0) {
8493
+ wrappedLine.prefixStyle = line.prefixStyle;
8494
+ }
8495
+ if (line.bodyStyle !== void 0) {
8496
+ wrappedLine.bodyStyle = line.bodyStyle;
8802
8497
  }
8803
- if (this.scrollOffset > 0) {
8804
- this.term(" \xB7 ").brightCyan(`\u2191 ${this.scrollOffset}`);
8498
+ if (line.fillRow) {
8499
+ wrappedLine.fillRow = true;
8805
8500
  }
8806
- const hint = this.banner.currentMode ? this.banner.hint.replace(
8807
- "\u21E7\u21E5 mode",
8808
- `\u21E7\u21E5 mode(${this.banner.currentMode})`
8809
- ) : this.banner.hint;
8810
- this.term(" \xB7 ").dim(hint);
8811
- if (right) {
8812
- const visibleWidth = stringWidth(right.text);
8813
- const col = Math.max(1, w - visibleWidth + 1);
8814
- this.term.moveTo(col, row).eraseLineAfter();
8815
- if (right.kind === "search") {
8816
- this.term.brightCyan.noFormat(right.text);
8817
- } else {
8818
- this.term.brightYellow.noFormat(right.text);
8819
- }
8501
+ if (line.ansi) {
8502
+ wrappedLine.ansi = true;
8820
8503
  }
8821
- });
8822
- }
8823
- placeCursor() {
8824
- if (!this.started) {
8825
- return;
8504
+ if (i === 0 && line.iterm2Image) {
8505
+ wrappedLine.iterm2Image = line.iterm2Image;
8506
+ }
8507
+ if (id !== void 0 && chunk.length > 0) {
8508
+ const found = line.body.indexOf(chunk, scanPos);
8509
+ const colOffset = found === -1 ? scanPos : found;
8510
+ this.wrapOrigin.set(wrappedLine, {
8511
+ sourceLineId: id,
8512
+ sourceColOffset: colOffset
8513
+ });
8514
+ scanPos = colOffset + chunk.length;
8515
+ }
8516
+ wrapped.push(wrappedLine);
8826
8517
  }
8827
- if (this.permissionPrompt) {
8828
- const rows = this.permissionRows();
8829
- const top2 = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
8830
- const optionRow = top2 + 3 + this.permissionPrompt.selectedIndex;
8831
- const lastUsableRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
8832
- this.term.moveTo(2, Math.min(optionRow, lastUsableRow));
8833
- return;
8518
+ if (id !== void 0) {
8519
+ this.wrapCache.set(id, wrapped);
8834
8520
  }
8835
- if (this.confirmPrompt) {
8836
- const top2 = this.term.height - CONFIRM_PROMPT_ROWS - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
8837
- this.term.moveTo(2, top2);
8838
- return;
8521
+ return wrapped;
8522
+ }
8523
+ writeFormattedLine(line, width, activeMatchCol = null, activeMatchLength = 0) {
8524
+ if (line.prefix) {
8525
+ writeStyled(this.term, line.prefix, line.prefixStyle ?? line.bodyStyle);
8839
8526
  }
8840
- if (this.helpPrompt) {
8841
- const rows = this.helpRows();
8842
- const top2 = this.term.height - rows - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
8843
- this.term.moveTo(2, top2);
8844
- return;
8527
+ const remaining = Math.max(0, width - (line.prefix?.length ?? 0));
8528
+ const stripMarkup = line.bodyStyle === "agent";
8529
+ const bodyText = line.ansi ? line.body : truncate(line.body, remaining, { stripMarkup });
8530
+ if (this.scrollbackHighlight !== null && !line.ansi) {
8531
+ writeBodyWithHighlight(
8532
+ this.term,
8533
+ bodyText,
8534
+ line.bodyStyle,
8535
+ this.scrollbackHighlight,
8536
+ activeMatchCol,
8537
+ activeMatchLength
8538
+ );
8539
+ } else {
8540
+ writeStyled(this.term, bodyText, line.bodyStyle);
8845
8541
  }
8846
- if (this.scrollbackSearch) {
8847
- this.term.hideCursor(true);
8848
- return;
8542
+ if (line.fillRow) {
8543
+ const visible = line.ansi ? stringWidth(bodyText) : bodyText.length;
8544
+ const pad = remaining - visible;
8545
+ if (pad > 0) {
8546
+ writeStyled(this.term, " ".repeat(pad), line.bodyStyle);
8547
+ }
8849
8548
  }
8850
- if (this.readonly) {
8851
- this.term.hideCursor(true);
8852
- return;
8549
+ if (line.ansi || line.body.includes("^")) {
8550
+ this.term.styleReset();
8853
8551
  }
8854
- this.term.hideCursor(false);
8855
- const w = this.term.width;
8856
- const room = Math.max(1, w - 2);
8857
- const state = this.dispatcher.state();
8858
- const visualRows = computePromptVisualRows(state.buffer, room);
8859
- const layout = computePromptLayout(visualRows, state, MAX_PROMPT_ROWS);
8860
- const top = this.term.height - layout.rendered - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS + 1;
8861
- const row = top + Math.max(0, layout.cursorVisualRow - layout.windowStart);
8862
- const col = layout.cursorVisualCol + 3;
8863
- const lastPromptRow = this.term.height - BANNER_ROWS - SEPARATOR_ROWS - SESSIONBAR_ROWS;
8864
- this.term.moveTo(
8865
- Math.min(col, this.term.width),
8866
- Math.min(row, lastPromptRow)
8867
- );
8552
+ if (line.iterm2Image && this.isIterm2()) {
8553
+ this.writeIterm2Image(
8554
+ line.iterm2Image.data,
8555
+ line.iterm2Image.heightCells
8556
+ );
8557
+ }
8558
+ }
8559
+ };
8560
+ NON_ASCII = /[^\x20-\x7e]/;
8561
+ SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
8562
+ TK_MARKUP_STYLE_CHAR = /[a-zA-Z+\-:_!#/]/;
8563
+ shortId = stripHydraSessionPrefix;
8564
+ }
8565
+ });
8566
+
8567
+ // src/tui/picker.ts
8568
+ async function pickSession(term, opts) {
8569
+ process.stdout.write("\x1B[<u");
8570
+ process.stdout.write("\x1B[?2004l");
8571
+ process.stdout.write("\x1B[>4;0m");
8572
+ process.stdout.write("\x1B[>5;0m");
8573
+ process.stdout.write("\x1B[?1000l");
8574
+ process.stdout.write("\x1B[?1002l");
8575
+ process.stdout.write("\x1B[?1006l");
8576
+ process.stdout.write("\x1B[?1l");
8577
+ process.stdout.write("\x1B>");
8578
+ const sortSessions = (sessions) => {
8579
+ const score = (s) => {
8580
+ if (s.status !== "live") {
8581
+ return 0;
8582
+ }
8583
+ return s.cwd === opts.cwd ? 2 : 1;
8584
+ };
8585
+ return [...sessions].sort((a, b) => {
8586
+ const tier = score(b) - score(a);
8587
+ if (tier !== 0) {
8588
+ return tier;
8589
+ }
8590
+ return b.updatedAt.localeCompare(a.updatedAt);
8591
+ });
8592
+ };
8593
+ let cwdOnly = false;
8594
+ let hostFilter = "__local";
8595
+ if (opts.currentSessionId !== void 0) {
8596
+ const current = opts.sessions.find(
8597
+ (s) => s.sessionId === opts.currentSessionId
8598
+ );
8599
+ if (current?.importedFromMachine) {
8600
+ hostFilter = "__all";
8601
+ }
8602
+ }
8603
+ let allSessions = sortSessions(opts.sessions);
8604
+ let visible = filterByHost(allSessions, hostFilter);
8605
+ let rows = visible.map((s) => toRow(s, Date.now()));
8606
+ let widths = computeWidths(rows);
8607
+ let total = 1 + visible.length;
8608
+ let selectedIdx = 0;
8609
+ let scrollOffset = 0;
8610
+ if (opts.currentSessionId !== void 0) {
8611
+ const idx = visible.findIndex((s) => s.sessionId === opts.currentSessionId);
8612
+ if (idx >= 0) {
8613
+ selectedIdx = idx + 1;
8614
+ }
8615
+ }
8616
+ let searchActive = false;
8617
+ let searchTerm = "";
8618
+ let mode = "normal";
8619
+ let pendingAction = null;
8620
+ let renameBuffer = "";
8621
+ let transientStatus = null;
8622
+ const composer = new InputDispatcher({ history: [] });
8623
+ let termHeight = readTermHeight(term);
8624
+ let termWidth = readTermWidth(term);
8625
+ let viewportSize = 0;
8626
+ let composerTitle = "";
8627
+ let composerRoom = 0;
8628
+ let composerVisualRows = [];
8629
+ let composerRows = 1;
8630
+ let composerWindowStart = 0;
8631
+ let composerCursorRow = 0;
8632
+ let composerCursorCol = 0;
8633
+ let headerLine = "";
8634
+ let sessionLines = [];
8635
+ let startRow = 1;
8636
+ const cwdMaxWidth = opts.config.tui.cwdColumnMaxWidth;
8637
+ const computeLayout = () => {
8638
+ termHeight = readTermHeight(term);
8639
+ termWidth = readTermWidth(term);
8640
+ const rowMaxWidth = Math.max(10, termWidth - ROW_PREFIX_WIDTH);
8641
+ composerRoom = Math.max(10, termWidth - BOX_HORIZONTAL_PAD);
8642
+ const titleBudget = Math.max(10, termWidth - 8);
8643
+ composerTitle = formatComposerTitle(opts.cwd, titleBudget);
8644
+ const state = composer.state();
8645
+ composerVisualRows = computePromptVisualRows(state.buffer, composerRoom);
8646
+ const layout = computePromptLayout(
8647
+ composerVisualRows,
8648
+ state,
8649
+ PICKER_COMPOSER_MAX_ROWS
8650
+ );
8651
+ composerRows = layout.rendered;
8652
+ composerWindowStart = layout.windowStart;
8653
+ composerCursorRow = layout.cursorVisualRow;
8654
+ composerCursorCol = layout.cursorVisualCol;
8655
+ const reserved = 6 + composerRows;
8656
+ const maxViewportRows = Math.max(3, termHeight - reserved);
8657
+ viewportSize = Math.min(visible.length, maxViewportRows);
8658
+ headerLine = formatRow(HEADER, widths, rowMaxWidth, cwdMaxWidth);
8659
+ sessionLines = rows.map((r) => formatRow(r, widths, rowMaxWidth, cwdMaxWidth));
8660
+ };
8661
+ const rebuildRows = () => {
8662
+ rows = visible.map((s) => toRow(s, Date.now()));
8663
+ widths = computeWidths(rows);
8664
+ total = 1 + visible.length;
8665
+ computeLayout();
8666
+ };
8667
+ const applyFilter = () => {
8668
+ let base = allSessions;
8669
+ if (cwdOnly) {
8670
+ base = base.filter((s) => s.cwd === opts.cwd);
8671
+ }
8672
+ base = filterByHost(base, hostFilter);
8673
+ if (searchActive && searchTerm.length > 0) {
8674
+ visible = base.filter((s) => matchesSearch(s, searchTerm));
8675
+ } else {
8676
+ visible = base;
8677
+ }
8678
+ rebuildRows();
8679
+ if (searchActive) {
8680
+ scrollOffset = 0;
8681
+ selectedIdx = visible.length > 0 ? 1 : 0;
8682
+ } else if (selectedIdx > total - 1) {
8683
+ selectedIdx = Math.max(0, total - 1);
8684
+ }
8685
+ if (scrollOffset + viewportSize > visible.length) {
8686
+ scrollOffset = Math.max(0, visible.length - viewportSize);
8687
+ }
8688
+ adjustScroll();
8689
+ };
8690
+ const adjustScroll = () => {
8691
+ if (selectedIdx === 0) {
8692
+ return;
8693
+ }
8694
+ const sessionIdx = selectedIdx - 1;
8695
+ if (sessionIdx < scrollOffset) {
8696
+ scrollOffset = sessionIdx;
8697
+ } else if (sessionIdx >= scrollOffset + viewportSize) {
8698
+ scrollOffset = sessionIdx - viewportSize + 1;
8699
+ } else if (scrollOffset + viewportSize > visible.length) {
8700
+ scrollOffset = Math.max(0, visible.length - viewportSize);
8701
+ }
8702
+ };
8703
+ const composerBoxInner = () => Math.max(2, termWidth - 2);
8704
+ const paintComposerTopBorder = () => {
8705
+ const inner = composerBoxInner();
8706
+ const focused = selectedIdx === 0;
8707
+ const titleFragment = `\u2500 ${composerTitle} `;
8708
+ const dashCount = Math.max(1, inner - titleFragment.length);
8709
+ const dashes = "\u2500".repeat(dashCount);
8710
+ if (focused) {
8711
+ term.brightCyan.noFormat("\u256D");
8712
+ term.brightCyan.bold.noFormat(titleFragment);
8713
+ term.brightCyan.noFormat(`${dashes}\u256E`);
8714
+ } else {
8715
+ term.dim.noFormat(`\u256D${titleFragment}${dashes}\u256E`);
8716
+ }
8717
+ };
8718
+ const paintComposerBottomBorder = () => {
8719
+ const inner = composerBoxInner();
8720
+ const dashes = "\u2500".repeat(inner);
8721
+ if (selectedIdx === 0) {
8722
+ term.brightCyan.noFormat(`\u2570${dashes}\u256F`);
8723
+ } else {
8724
+ term.dim.noFormat(`\u2570${dashes}\u256F`);
8725
+ }
8726
+ };
8727
+ const paintComposerBodyRow = (visualIdx) => {
8728
+ const inner = composerBoxInner();
8729
+ const sideStyle = selectedIdx === 0 ? term.brightCyan : term.dim;
8730
+ sideStyle.noFormat("\u2502");
8731
+ const vr = composerVisualRows[visualIdx];
8732
+ let slice = "";
8733
+ if (vr) {
8734
+ slice = (composer.state().buffer[vr.bufferIdx] ?? "").slice(
8735
+ vr.startCol,
8736
+ vr.endCol
8737
+ );
8738
+ }
8739
+ term.noFormat(" ");
8740
+ term.noFormat(slice);
8741
+ const padWidth = Math.max(0, inner - 1 - slice.length);
8742
+ if (padWidth > 0) {
8743
+ term.noFormat(" ".repeat(padWidth));
8744
+ }
8745
+ sideStyle.noFormat("\u2502");
8746
+ };
8747
+ const paintSessionRow = (sessionIdx) => {
8748
+ const label = sessionLines[sessionIdx] ?? "";
8749
+ if (selectedIdx === sessionIdx + 1) {
8750
+ term.brightWhite.bgBlue.noFormat(`\u276F ${label}`);
8751
+ } else {
8752
+ term.noFormat(` ${label}`);
8753
+ }
8754
+ };
8755
+ const formatIndicator = () => {
8756
+ const above = scrollOffset;
8757
+ const below = Math.max(0, visible.length - scrollOffset - viewportSize);
8758
+ const parts = [];
8759
+ if (cwdOnly) {
8760
+ parts.push("cwd-only");
8761
+ }
8762
+ if (hostFilter !== "__all") {
8763
+ parts.push(
8764
+ hostFilter === "__local" ? "host: local" : `host: ${hostFilter}`
8765
+ );
8766
+ }
8767
+ if (above > 0) {
8768
+ parts.push(`\u2191 ${above} above`);
8769
+ }
8770
+ if (below > 0) {
8771
+ parts.push(`\u2193 ${below} below`);
8772
+ }
8773
+ if (parts.length === 0) {
8774
+ return "";
8775
+ }
8776
+ return ` ${parts.join(" \xB7 ")}`;
8777
+ };
8778
+ const shortId2 = (sessionId) => stripHydraSessionPrefix(sessionId);
8779
+ const paintIndicator = () => {
8780
+ term.moveTo(1, indicatorRow()).eraseLineAfter();
8781
+ if (mode === "confirm-kill" && pendingAction) {
8782
+ term.brightYellow.noFormat(` kill ${shortId2(pendingAction.sessionId)}? [y/N]`);
8783
+ return;
8784
+ }
8785
+ if (mode === "confirm-delete" && pendingAction) {
8786
+ term.brightRed.noFormat(` delete ${shortId2(pendingAction.sessionId)}? [y/N]`);
8787
+ return;
8788
+ }
8789
+ if (mode === "busy" && pendingAction) {
8790
+ term.dim.noFormat(` working on ${shortId2(pendingAction.sessionId)}\u2026`);
8791
+ return;
8792
+ }
8793
+ if (mode === "rename" && pendingAction) {
8794
+ term.brightYellow.noFormat(` title: ${renameBuffer}`);
8795
+ term.bgBrightYellow(" ");
8796
+ term.dim.noFormat(" Enter saves \xB7 Esc cancels");
8797
+ return;
8798
+ }
8799
+ if (transientStatus !== null) {
8800
+ term.dim.noFormat(` ${transientStatus}`);
8801
+ return;
8802
+ }
8803
+ if (searchActive) {
8804
+ term.brightYellow.noFormat(` /${searchTerm}`);
8805
+ term.bgBrightYellow(" ");
8806
+ const hint = visible.length === 0 ? " no matches" : ` ${visible.length} match${visible.length === 1 ? "" : "es"}`;
8807
+ term.dim.noFormat(`${hint} \xB7 ^c clears`);
8808
+ return;
8809
+ }
8810
+ term.dim.noFormat(formatIndicator());
8811
+ };
8812
+ const composerBodyRow = (visualOffset) => startRow + 1 + visualOffset;
8813
+ const composerBottomRow = () => startRow + composerRows + 1;
8814
+ const headerRow = () => startRow + composerRows + 3;
8815
+ const sessionRow = (sessionIdx) => headerRow() + 1 + (sessionIdx - scrollOffset);
8816
+ const indicatorRow = () => headerRow() + 1 + viewportSize;
8817
+ const placeComposerCursor = () => {
8818
+ const visualOffset = composerCursorRow - composerWindowStart;
8819
+ if (visualOffset < 0 || visualOffset >= composerRows) {
8820
+ return;
8821
+ }
8822
+ const col = 3 + composerCursorCol;
8823
+ term.moveTo(col, composerBodyRow(visualOffset));
8824
+ };
8825
+ const renderFromScratch = () => {
8826
+ if (mode === "help") {
8827
+ renderHelp();
8828
+ return;
8829
+ }
8830
+ computeLayout();
8831
+ adjustScroll();
8832
+ startRow = 1;
8833
+ term.moveTo(1, 1).eraseDisplayBelow();
8834
+ paintComposerTopBorder();
8835
+ term("\n");
8836
+ for (let v = 0; v < composerRows; v++) {
8837
+ paintComposerBodyRow(composerWindowStart + v);
8838
+ term("\n");
8839
+ }
8840
+ paintComposerBottomBorder();
8841
+ term("\n\n");
8842
+ term.dim.noFormat(` ${headerLine}`)("\n");
8843
+ for (let v = 0; v < viewportSize; v++) {
8844
+ paintSessionRow(scrollOffset + v);
8845
+ term("\n");
8846
+ }
8847
+ paintIndicator();
8848
+ term("\n");
8849
+ if (selectedIdx === 0) {
8850
+ placeComposerCursor();
8851
+ term.hideCursor(false);
8852
+ } else {
8853
+ term.hideCursor();
8854
+ }
8855
+ };
8856
+ const renderHelp = () => {
8857
+ term.moveTo(1, 1).eraseDisplayBelow();
8858
+ term.brightWhite.bold.noFormat(" Picker hotkeys")("\n\n");
8859
+ for (const entry of HELP_ENTRIES) {
8860
+ if (entry === null) {
8861
+ term("\n");
8862
+ continue;
8868
8863
  }
8869
- promptRows() {
8870
- if (this.permissionPrompt) {
8871
- return this.permissionRows();
8872
- }
8873
- if (this.confirmPrompt) {
8874
- return CONFIRM_PROMPT_ROWS;
8875
- }
8876
- if (this.helpPrompt) {
8877
- return this.helpRows();
8878
- }
8879
- if (this.readonly) {
8880
- return 0;
8881
- }
8882
- const w = this.term.width;
8883
- const room = Math.max(1, w - 2);
8884
- const state = this.dispatcher.state();
8885
- const visualRows = computePromptVisualRows(state.buffer, room);
8886
- return Math.min(MAX_PROMPT_ROWS, Math.max(1, visualRows.length));
8864
+ const [keys, desc] = entry;
8865
+ term.brightCyan.noFormat(` ${keys.padEnd(HELP_KEYS_WIDTH)}`);
8866
+ term.noFormat(desc)("\n");
8867
+ }
8868
+ term("\n");
8869
+ term.dim.noFormat(" press any key to dismiss")("\n");
8870
+ };
8871
+ const repaintComposerChrome = () => {
8872
+ term.moveTo(1, startRow).eraseLineAfter();
8873
+ paintComposerTopBorder();
8874
+ term.moveTo(1, composerBottomRow()).eraseLineAfter();
8875
+ paintComposerBottomBorder();
8876
+ for (let v = 0; v < composerRows; v++) {
8877
+ term.moveTo(1, composerBodyRow(v)).eraseLineAfter();
8878
+ paintComposerBodyRow(composerWindowStart + v);
8879
+ }
8880
+ };
8881
+ const repaintComposerBody = () => {
8882
+ const state = composer.state();
8883
+ composerVisualRows = computePromptVisualRows(state.buffer, composerRoom);
8884
+ const layout = computePromptLayout(
8885
+ composerVisualRows,
8886
+ state,
8887
+ PICKER_COMPOSER_MAX_ROWS
8888
+ );
8889
+ composerWindowStart = layout.windowStart;
8890
+ composerCursorRow = layout.cursorVisualRow;
8891
+ composerCursorCol = layout.cursorVisualCol;
8892
+ for (let v = 0; v < composerRows; v++) {
8893
+ term.moveTo(1, composerBodyRow(v)).eraseLineAfter();
8894
+ paintComposerBodyRow(composerWindowStart + v);
8895
+ }
8896
+ if (selectedIdx === 0) {
8897
+ placeComposerCursor();
8898
+ }
8899
+ };
8900
+ const repaintSessionRow = (sessionIdx) => {
8901
+ if (sessionIdx < scrollOffset || sessionIdx >= scrollOffset + viewportSize) {
8902
+ return;
8903
+ }
8904
+ term.moveTo(1, sessionRow(sessionIdx)).eraseLineAfter();
8905
+ paintSessionRow(sessionIdx);
8906
+ };
8907
+ const repaintViewport = () => {
8908
+ for (let v = 0; v < viewportSize; v++) {
8909
+ const row = headerRow() + 1 + v;
8910
+ term.moveTo(1, row).eraseLineAfter();
8911
+ const sessionIdx = scrollOffset + v;
8912
+ if (sessionIdx < visible.length) {
8913
+ paintSessionRow(sessionIdx);
8887
8914
  }
8888
- permissionRows() {
8889
- if (!this.permissionPrompt) {
8890
- return 0;
8891
- }
8892
- return Math.min(
8893
- MAX_PERMISSION_ROWS,
8894
- 4 + this.permissionPrompt.options.length
8895
- );
8915
+ }
8916
+ paintIndicator();
8917
+ };
8918
+ renderFromScratch();
8919
+ return await new Promise((resolve6) => {
8920
+ let resolved = false;
8921
+ const onResize = () => {
8922
+ if (resolved) {
8923
+ return;
8896
8924
  }
8897
- // Walk this.lines from the tail, accumulating wrapped rows via the
8898
- // wrap cache, until we have at least `needed` rows or run out. Returns
8899
- // the collected rows in original (top-down) order plus an `exhausted`
8900
- // flag that's true iff we reached the head of this.lines. The hot path
8901
- // (drawScrollback) only ever asks for `visibleRows + scrollOffset`
8902
- // rows, so a 10k-line scrollback costs ~50 cache hits per repaint
8903
- // instead of 10k. With `needed = Infinity` this walks everything and
8904
- // doubles as a total-row counter for maxScrollOffset.
8905
- wrapTail(width, needed) {
8906
- if (width <= 4) {
8907
- const take = Math.min(needed, this.lines.length);
8908
- return {
8909
- rows: this.lines.slice(this.lines.length - take),
8910
- exhausted: needed >= this.lines.length
8911
- };
8912
- }
8913
- if (this.wrapCacheWidth !== width) {
8914
- this.wrapCache.clear();
8915
- this.wrapCacheWidth = width;
8916
- }
8917
- if (needed <= 0 || this.lines.length === 0) {
8918
- return { rows: [], exhausted: true };
8919
- }
8920
- const batches = [];
8921
- let total = 0;
8922
- let stoppedAt = 0;
8923
- for (let i = this.lines.length - 1; i >= 0; i--) {
8924
- const wrapped = this.wrapOne(this.lines[i], width);
8925
- batches.push(wrapped);
8926
- total += wrapped.length;
8927
- stoppedAt = i;
8928
- if (total >= needed) {
8929
- break;
8930
- }
8931
- }
8932
- const rows = [];
8933
- for (let i = batches.length - 1; i >= 0; i--) {
8934
- rows.push(...batches[i]);
8935
- }
8936
- return { rows, exhausted: stoppedAt === 0 };
8925
+ renderFromScratch();
8926
+ };
8927
+ const cleanup = () => {
8928
+ if (resolved) {
8929
+ return;
8937
8930
  }
8938
- wrapOne(line, width) {
8939
- const id = this.lineIds.get(line);
8940
- if (id !== void 0) {
8941
- const cached2 = this.wrapCache.get(id);
8942
- if (cached2) {
8943
- return cached2;
8944
- }
8945
- }
8946
- const prefix = line.prefix ?? "";
8947
- const room = Math.max(1, width - prefix.length);
8948
- const stripMarkup = line.bodyStyle === "agent";
8949
- const chunks = line.ansi ? wrapAnsiBody(line.body, room) : wrap(line.body, room, { stripMarkup });
8950
- const wrapped = [];
8951
- let scanPos = 0;
8952
- for (let i = 0; i < chunks.length; i++) {
8953
- const chunk = chunks[i] ?? "";
8954
- const wrappedLine = {
8955
- prefix: i === 0 ? line.prefix : " ".repeat(prefix.length),
8956
- body: chunk
8957
- };
8958
- if (line.prefixStyle !== void 0) {
8959
- wrappedLine.prefixStyle = line.prefixStyle;
8960
- }
8961
- if (line.bodyStyle !== void 0) {
8962
- wrappedLine.bodyStyle = line.bodyStyle;
8963
- }
8964
- if (line.fillRow) {
8965
- wrappedLine.fillRow = true;
8966
- }
8967
- if (line.ansi) {
8968
- wrappedLine.ansi = true;
8969
- }
8970
- if (i === 0 && line.iterm2Image) {
8971
- wrappedLine.iterm2Image = line.iterm2Image;
8972
- }
8973
- if (id !== void 0 && chunk.length > 0) {
8974
- const found = line.body.indexOf(chunk, scanPos);
8975
- const colOffset = found === -1 ? scanPos : found;
8976
- this.wrapOrigin.set(wrappedLine, {
8977
- sourceLineId: id,
8978
- sourceColOffset: colOffset
8979
- });
8980
- scanPos = colOffset + chunk.length;
8931
+ resolved = true;
8932
+ term.off("key", onKey);
8933
+ term.off("resize", onResize);
8934
+ term.grabInput(false);
8935
+ term.hideCursor(false);
8936
+ term.moveTo(1, indicatorRow() + 1);
8937
+ term("\n");
8938
+ };
8939
+ const refresh = async (preferredId) => {
8940
+ try {
8941
+ const next = await listSessions(opts.target);
8942
+ allSessions = sortSessions(next);
8943
+ applyFilter();
8944
+ if (preferredId !== void 0) {
8945
+ const idx = visible.findIndex((s) => s.sessionId === preferredId);
8946
+ if (idx >= 0) {
8947
+ selectedIdx = idx + 1;
8981
8948
  }
8982
- wrapped.push(wrappedLine);
8983
8949
  }
8984
- if (id !== void 0) {
8985
- this.wrapCache.set(id, wrapped);
8950
+ if (selectedIdx > total - 1) {
8951
+ selectedIdx = Math.max(0, total - 1);
8986
8952
  }
8987
- return wrapped;
8988
- }
8989
- writeFormattedLine(line, width, activeMatchCol = null, activeMatchLength = 0) {
8990
- if (line.prefix) {
8991
- writeStyled(this.term, line.prefix, line.prefixStyle ?? line.bodyStyle);
8953
+ if (scrollOffset + viewportSize > visible.length) {
8954
+ scrollOffset = Math.max(0, visible.length - viewportSize);
8992
8955
  }
8993
- const remaining = Math.max(0, width - (line.prefix?.length ?? 0));
8994
- const stripMarkup = line.bodyStyle === "agent";
8995
- const bodyText = line.ansi ? line.body : truncate3(line.body, remaining, { stripMarkup });
8996
- if (this.scrollbackHighlight !== null && !line.ansi) {
8997
- writeBodyWithHighlight(
8998
- this.term,
8999
- bodyText,
9000
- line.bodyStyle,
9001
- this.scrollbackHighlight,
9002
- activeMatchCol,
9003
- activeMatchLength
9004
- );
8956
+ adjustScroll();
8957
+ renderFromScratch();
8958
+ } catch (err) {
8959
+ transientStatus = `refresh failed: ${err.message}`;
8960
+ renderFromScratch();
8961
+ }
8962
+ };
8963
+ const performRename = async (title) => {
8964
+ if (!pendingAction) {
8965
+ return;
8966
+ }
8967
+ const session = pendingAction;
8968
+ mode = "busy";
8969
+ paintIndicator();
8970
+ try {
8971
+ await renameSession(opts.target, session.sessionId, title);
8972
+ mode = "normal";
8973
+ pendingAction = null;
8974
+ renameBuffer = "";
8975
+ await refresh(session.sessionId);
8976
+ } catch (err) {
8977
+ mode = "normal";
8978
+ pendingAction = null;
8979
+ renameBuffer = "";
8980
+ transientStatus = `rename failed: ${err.message}`;
8981
+ paintIndicator();
8982
+ }
8983
+ };
8984
+ const performRegen = async (session) => {
8985
+ try {
8986
+ await regenSessionTitle(opts.target, session.sessionId);
8987
+ transientStatus = "title regen queued (press r to refresh)";
8988
+ paintIndicator();
8989
+ } catch (err) {
8990
+ transientStatus = `regen failed: ${err.message}`;
8991
+ paintIndicator();
8992
+ }
8993
+ };
8994
+ const performAction = async (kind) => {
8995
+ if (!pendingAction) {
8996
+ return;
8997
+ }
8998
+ const session = pendingAction;
8999
+ mode = "busy";
9000
+ paintIndicator();
9001
+ try {
9002
+ if (kind === "kill") {
9003
+ await killSession(opts.target, session.sessionId);
9005
9004
  } else {
9006
- writeStyled(this.term, bodyText, line.bodyStyle);
9007
- }
9008
- if (line.fillRow) {
9009
- const visible = line.ansi ? stringWidth(bodyText) : bodyText.length;
9010
- const pad = remaining - visible;
9011
- if (pad > 0) {
9012
- writeStyled(this.term, " ".repeat(pad), line.bodyStyle);
9013
- }
9014
- }
9015
- if (line.ansi || line.body.includes("^")) {
9016
- this.term.styleReset();
9017
- }
9018
- if (line.iterm2Image && this.isIterm2()) {
9019
- this.writeIterm2Image(
9020
- line.iterm2Image.data,
9021
- line.iterm2Image.heightCells
9022
- );
9005
+ await deleteSession(opts.target, session.sessionId);
9023
9006
  }
9007
+ mode = "normal";
9008
+ pendingAction = null;
9009
+ await refresh(kind === "kill" ? session.sessionId : void 0);
9010
+ } catch (err) {
9011
+ mode = "normal";
9012
+ pendingAction = null;
9013
+ transientStatus = `${kind} failed: ${err.message}`;
9014
+ paintIndicator();
9024
9015
  }
9025
9016
  };
9026
- NON_ASCII = /[^\x20-\x7e]/;
9027
- SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
9028
- TK_MARKUP_STYLE_CHAR = /[a-zA-Z+\-:_!#/]/;
9029
- shortId = stripHydraSessionPrefix;
9030
- }
9031
- });
9032
-
9033
- // src/tui/input.ts
9034
- var InputDispatcher;
9035
- var init_input = __esm({
9036
- "src/tui/input.ts"() {
9037
- "use strict";
9038
- InputDispatcher = class {
9039
- buffer = [""];
9040
- row = 0;
9041
- col = 0;
9042
- planMode = false;
9043
- historyIndex = -1;
9044
- // Queue editing: when the user walks Up past row 0 with queued prompts
9045
- // present, the most-recently-queued item lands in the buffer and
9046
- // queueIndex tracks which slot of `queue` is being edited. Enter submits
9047
- // the edit (queue-edit) or, on an empty buffer, drops the slot
9048
- // (queue-remove). -1 means not editing a queue slot.
9049
- queueIndex = -1;
9050
- savedDraft = null;
9051
- history = [];
9052
- // Active reverse-incremental search over `history`. Set when ^r is
9053
- // pressed; cleared when the user accepts (Enter / typing / arrows)
9054
- // or cancels (ESC). `query` is the lowercased substring matched
9055
- // against history entries; `matchIndices` are history indices in
9056
- // newest→oldest order; `cursor` is the current index into that list.
9057
- // `savedDraft` snapshots the buffer/cursor at the moment search
9058
- // began so ESC can restore it.
9059
- historySearch = null;
9060
- // Waiting queue snapshot (excludes the in-flight head). Newest item lives
9061
- // at the end so Up walks the array right-to-left.
9062
- queue = [];
9063
- turnRunning = false;
9064
- // Single-slot kill ring. The most recent killed text (^U, ^K, ^W) lands
9065
- // here so ^Y can yank it back. Standard readline keeps a stack; we
9066
- // only keep one slot because that's what 99% of yank uses look like.
9067
- killBuffer = "";
9068
- // Images attached to the current draft. Cleared in the same paths
9069
- // that clear the text buffer (clearBuffer, after send). Queue
9070
- // navigation snapshots/restores them alongside savedDraft so up/down
9071
- // through queued items doesn't drop chips.
9072
- attachments = [];
9073
- // Snapshot of `attachments` taken when the user starts walking
9074
- // history/queue with chips already attached. Restored alongside the
9075
- // text draft when the walk ends. Distinct from savedDraft because
9076
- // queue slots (which may carry their own attachments — though we
9077
- // don't surface that yet) shouldn't blend with the current draft's.
9078
- savedAttachments = null;
9079
- constructor(opts = {}) {
9080
- this.history = [...opts.history ?? []];
9081
- this.planMode = opts.planMode ?? false;
9017
+ const onFocusChange = (oldIdx, newIdx) => {
9018
+ if (oldIdx === 0 === (newIdx === 0)) {
9019
+ return;
9082
9020
  }
9083
- state() {
9084
- return {
9085
- buffer: [...this.buffer],
9086
- row: this.row,
9087
- col: this.col,
9088
- planMode: this.planMode,
9089
- historyIndex: this.historyIndex,
9090
- queueIndex: this.queueIndex,
9091
- attachments: [...this.attachments],
9092
- historySearchQuery: this.historySearch?.query ?? null
9093
- };
9021
+ repaintComposerChrome();
9022
+ if (newIdx === 0) {
9023
+ term.hideCursor(false);
9024
+ placeComposerCursor();
9025
+ } else {
9026
+ term.hideCursor();
9094
9027
  }
9095
- // App calls this after asynchronously acquiring an image (drag-drop
9096
- // file read, clipboard shellout). The dispatcher just records it;
9097
- // chip rendering and capability gating live in the app/screen layer.
9098
- addAttachment(attachment) {
9099
- this.attachments.push(attachment);
9028
+ };
9029
+ const move = (delta) => {
9030
+ const next = Math.min(total - 1, Math.max(0, selectedIdx + delta));
9031
+ if (next === selectedIdx) {
9032
+ return;
9100
9033
  }
9101
- removeAttachment(index) {
9102
- if (index < 0 || index >= this.attachments.length) {
9034
+ const old = selectedIdx;
9035
+ const oldScroll = scrollOffset;
9036
+ selectedIdx = next;
9037
+ adjustScroll();
9038
+ if (scrollOffset !== oldScroll) {
9039
+ repaintViewport();
9040
+ onFocusChange(old, selectedIdx);
9041
+ return;
9042
+ }
9043
+ if (old !== 0) {
9044
+ repaintSessionRow(old - 1);
9045
+ }
9046
+ if (selectedIdx !== 0) {
9047
+ repaintSessionRow(selectedIdx - 1);
9048
+ }
9049
+ onFocusChange(old, selectedIdx);
9050
+ };
9051
+ const clearTransient = () => {
9052
+ if (transientStatus === null) {
9053
+ return false;
9054
+ }
9055
+ transientStatus = null;
9056
+ paintIndicator();
9057
+ return true;
9058
+ };
9059
+ const onKey = (name, _matches, data) => {
9060
+ if (mode === "busy") {
9061
+ return;
9062
+ }
9063
+ if (mode === "help") {
9064
+ if (name === "CTRL_C") {
9065
+ cleanup();
9066
+ resolve6({ kind: "abort" });
9067
+ return;
9068
+ }
9069
+ mode = "normal";
9070
+ renderFromScratch();
9071
+ return;
9072
+ }
9073
+ if (mode === "rename") {
9074
+ if (name === "ENTER" || name === "KP_ENTER") {
9075
+ const trimmed = renameBuffer.trim();
9076
+ if (trimmed.length === 0) {
9077
+ mode = "normal";
9078
+ pendingAction = null;
9079
+ renameBuffer = "";
9080
+ paintIndicator();
9081
+ return;
9082
+ }
9083
+ void performRename(trimmed);
9084
+ return;
9085
+ }
9086
+ if (name === "ESCAPE" || name === "CTRL_C") {
9087
+ mode = "normal";
9088
+ pendingAction = null;
9089
+ renameBuffer = "";
9090
+ paintIndicator();
9091
+ return;
9092
+ }
9093
+ if (name === "BACKSPACE") {
9094
+ if (renameBuffer.length > 0) {
9095
+ renameBuffer = renameBuffer.slice(0, -1);
9096
+ paintIndicator();
9097
+ }
9098
+ return;
9099
+ }
9100
+ if (name === "CTRL_U") {
9101
+ renameBuffer = "";
9102
+ paintIndicator();
9103
9103
  return;
9104
9104
  }
9105
- this.attachments.splice(index, 1);
9106
- }
9107
- setTurnRunning(running) {
9108
- this.turnRunning = running;
9109
- }
9110
- setHistory(history) {
9111
- this.history = [...history];
9112
- this.historyIndex = -1;
9113
- this.savedDraft = null;
9114
- this.historySearch = null;
9115
- }
9116
- // Snapshot of the waiting queue (head excluded). Called by the app after
9117
- // every queue mutation so Up/Down can walk a fresh view. queueIndex is
9118
- // only invalidated when it falls outside the new bounds — staying in
9119
- // bounds preserves the user's edit if the queue grew or stayed put.
9120
- setQueue(queue) {
9121
- this.queue = [...queue];
9122
- if (this.queueIndex >= this.queue.length) {
9123
- this.queueIndex = -1;
9105
+ if (name === "CTRL_W") {
9106
+ const trimmedRight = renameBuffer.replace(/\s+$/, "");
9107
+ const lastSpace = trimmedRight.lastIndexOf(" ");
9108
+ renameBuffer = lastSpace >= 0 ? trimmedRight.slice(0, lastSpace) : "";
9109
+ paintIndicator();
9110
+ return;
9124
9111
  }
9125
- }
9126
- // Replace the contents of the first row, leaving subsequent rows alone.
9127
- // Used by slash-command completion: the partial /foo gets swapped for the
9128
- // matched command name. Cursor moves to the end of the replacement.
9129
- replaceFirstLine(text) {
9130
- this.buffer[0] = text;
9131
- if (this.row === 0) {
9132
- this.col = text.length;
9112
+ if (data?.isCharacter) {
9113
+ renameBuffer += name;
9114
+ paintIndicator();
9115
+ return;
9133
9116
  }
9117
+ return;
9134
9118
  }
9135
- // Public seed for the buffer (used for Escape pre-fill). Treated like a
9136
- // fresh draft: nav state and any saved draft are cleared, cursor lands
9137
- // at the end so the user can edit immediately. Attachments restore
9138
- // alongside the text so a cancelled turn's chips land back in the
9139
- // draft together with the typed prompt.
9140
- setBuffer(text, attachments = []) {
9141
- this.loadEntry(text);
9142
- this.historyIndex = -1;
9143
- this.queueIndex = -1;
9144
- this.savedDraft = null;
9145
- this.savedAttachments = null;
9146
- this.historySearch = null;
9147
- this.attachments = [...attachments];
9119
+ if (mode === "confirm-kill" || mode === "confirm-delete") {
9120
+ if (data?.isCharacter && (name === "y" || name === "Y")) {
9121
+ const kind = mode === "confirm-kill" ? "kill" : "delete";
9122
+ void performAction(kind);
9123
+ return;
9124
+ }
9125
+ if (name === "ESCAPE" || name === "CTRL_C" || name === "ENTER" || name === "KP_ENTER" || data?.isCharacter && (name === "n" || name === "N")) {
9126
+ mode = "normal";
9127
+ pendingAction = null;
9128
+ paintIndicator();
9129
+ return;
9130
+ }
9131
+ return;
9148
9132
  }
9149
- feed(event) {
9150
- if (this.historySearch !== null) {
9151
- if (event.type === "char") {
9152
- return this.mutateHistorySearchQuery(
9153
- this.historySearch.query + event.ch.toLowerCase()
9154
- );
9155
- }
9156
- if (event.type === "paste") {
9157
- return this.mutateHistorySearchQuery(
9158
- this.historySearch.query + event.text.replace(/\n/g, " ").toLowerCase()
9159
- );
9160
- }
9161
- if (event.type === "key") {
9162
- if (event.name === "ctrl-r") {
9163
- return this.advanceHistorySearch();
9164
- }
9165
- if (event.name === "ctrl-s") {
9166
- this.retreatHistorySearch();
9167
- return [];
9168
- }
9169
- if (event.name === "escape" || event.name === "ctrl-c") {
9170
- this.cancelHistorySearch();
9171
- return [];
9172
- }
9173
- if (event.name === "backspace") {
9174
- if (this.historySearch.query.length === 0) {
9175
- this.cancelHistorySearch();
9176
- return [];
9177
- }
9178
- return this.mutateHistorySearchQuery(
9179
- this.historySearch.query.slice(0, -1)
9180
- );
9181
- }
9182
- this.historySearch = null;
9183
- }
9133
+ clearTransient();
9134
+ if (selectedIdx === 0 && !searchActive) {
9135
+ if (name === "ESCAPE" || name === "CTRL_C" || name === "CTRL_D") {
9136
+ cleanup();
9137
+ resolve6({ kind: "abort" });
9138
+ return;
9184
9139
  }
9185
- if (event.type === "char") {
9186
- this.insertChar(event.ch);
9187
- return [];
9140
+ if (name === "ENTER" || name === "KP_ENTER") {
9141
+ cleanup();
9142
+ const text = composer.state().buffer.join("\n");
9143
+ if (text.trim().length === 0) {
9144
+ resolve6({ kind: "new" });
9145
+ } else {
9146
+ resolve6({ kind: "new", prompt: text });
9147
+ }
9148
+ return;
9188
9149
  }
9189
- if (event.type === "paste") {
9190
- this.insertText(event.text);
9191
- return [];
9150
+ if (name === "DOWN") {
9151
+ const atBottom = composerVisualRows.length === 0 || composerCursorRow === composerVisualRows.length - 1;
9152
+ if (atBottom && visible.length > 0) {
9153
+ move(1);
9154
+ return;
9155
+ }
9192
9156
  }
9193
- if (event.type === "attachment-paths") {
9194
- return [];
9157
+ if (name === "PAGE_DOWN") {
9158
+ const atBottom = composerVisualRows.length === 0 || composerCursorRow === composerVisualRows.length - 1;
9159
+ if (atBottom && visible.length > 0) {
9160
+ move(1);
9161
+ return;
9162
+ }
9195
9163
  }
9196
- return this.handleKey(event.name);
9197
- }
9198
- handleKey(name) {
9199
- switch (name) {
9200
- case "enter":
9201
- return this.send();
9202
- case "shift-enter":
9203
- case "ctrl-enter":
9204
- return this.amend();
9205
- case "alt-enter":
9206
- this.insertNewline();
9207
- return [];
9208
- case "shift-tab":
9209
- this.planMode = !this.planMode;
9210
- return [
9211
- { type: "plan-toggle", on: this.planMode },
9212
- { type: "redraw-banner" }
9213
- ];
9214
- case "tab":
9215
- this.insertText(" ");
9216
- return [];
9217
- case "up":
9218
- return this.handleUp();
9219
- case "down":
9220
- return this.handleDown();
9221
- case "left":
9222
- this.moveLeft();
9223
- return [];
9224
- case "right":
9225
- this.moveRight();
9226
- return [];
9227
- case "ctrl-a":
9228
- this.col = 0;
9229
- return [];
9230
- case "ctrl-e":
9231
- this.col = this.currentLine().length;
9232
- return [];
9233
- case "home":
9234
- return this.handleHome();
9235
- case "end":
9236
- return this.handleEnd();
9237
- case "ctrl-b":
9238
- this.moveLeft();
9239
- return [];
9240
- case "ctrl-f":
9241
- this.moveRight();
9242
- return [];
9243
- case "ctrl-g":
9244
- return [{ type: "show-help" }];
9245
- case "alt-b":
9246
- this.moveWordBackward();
9247
- return [];
9248
- case "alt-f":
9249
- this.moveWordForward();
9250
- return [];
9251
- case "ctrl-k":
9252
- this.killToEnd();
9253
- return [];
9254
- case "ctrl-n":
9255
- return this.handleDown();
9256
- case "ctrl-o":
9257
- return [{ type: "toggle-tools" }];
9258
- case "backspace":
9259
- this.backspace();
9260
- return [];
9261
- case "delete":
9262
- this.deleteForward();
9263
- return [];
9264
- case "ctrl-c":
9265
- return this.handleCtrlC();
9266
- case "ctrl-d":
9267
- if (this.bufferIsEmpty()) {
9268
- return [{ type: "exit" }];
9269
- }
9270
- this.deleteForward();
9271
- return [];
9272
- case "ctrl-l":
9273
- return [{ type: "redraw" }];
9274
- case "ctrl-p":
9275
- return [{ type: "switch-session" }];
9276
- case "ctrl-t":
9277
- return [{ type: "next-live-session" }];
9278
- case "ctrl-r":
9279
- return this.startHistorySearch();
9280
- case "ctrl-s":
9281
- return [];
9282
- case "ctrl-u":
9283
- this.killLine();
9284
- return [];
9285
- case "ctrl-v":
9286
- return [{ type: "attachment-request", source: "clipboard" }];
9287
- case "ctrl-w":
9288
- this.killWord();
9289
- return [];
9290
- case "ctrl-x":
9291
- return [{ type: "toggle-mouse" }];
9292
- case "ctrl-y":
9293
- this.yank();
9294
- return [];
9295
- case "escape":
9296
- if (this.turnRunning) {
9297
- return [{ type: "cancel", prefill: true }];
9298
- }
9299
- return [];
9164
+ if (name === "CTRL_P") {
9165
+ if (visible.length > 0) {
9166
+ move(1);
9167
+ }
9168
+ return;
9300
9169
  }
9301
- }
9302
- currentLine() {
9303
- return this.buffer[this.row] ?? "";
9304
- }
9305
- setCurrentLine(line) {
9306
- this.buffer[this.row] = line;
9307
- }
9308
- bufferText() {
9309
- return this.buffer.join("\n");
9310
- }
9311
- bufferIsEmpty() {
9312
- return this.buffer.length === 1 && this.buffer[0] === "";
9313
- }
9314
- clearBuffer() {
9315
- this.buffer = [""];
9316
- this.row = 0;
9317
- this.col = 0;
9318
- this.historyIndex = -1;
9319
- this.queueIndex = -1;
9320
- this.savedDraft = null;
9321
- this.savedAttachments = null;
9322
- this.historySearch = null;
9323
- this.attachments = [];
9324
- }
9325
- insertChar(ch) {
9326
- if (ch.length === 0) {
9170
+ const before = composer.state();
9171
+ let event = null;
9172
+ if (data?.isCharacter) {
9173
+ event = { type: "char", ch: name };
9174
+ } else {
9175
+ const mapped = mapKeyName(name);
9176
+ if (mapped !== null) {
9177
+ event = { type: "key", name: mapped };
9178
+ }
9179
+ }
9180
+ if (event === null) {
9181
+ placeComposerCursor();
9327
9182
  return;
9328
9183
  }
9329
- if (ch.includes("\n")) {
9330
- this.insertText(ch);
9184
+ composer.feed(event);
9185
+ const after = composer.state();
9186
+ const unchanged = before.buffer.length === after.buffer.length && before.buffer.every((line, i) => line === after.buffer[i]) && before.row === after.row && before.col === after.col;
9187
+ if (unchanged) {
9188
+ placeComposerCursor();
9331
9189
  return;
9332
9190
  }
9333
- const line = this.currentLine();
9334
- this.setCurrentLine(line.slice(0, this.col) + ch + line.slice(this.col));
9335
- this.col += ch.length;
9336
- }
9337
- insertText(text) {
9338
- const lines = text.split("\n");
9339
- if (lines.length === 1) {
9340
- this.insertChar(lines[0] ?? "");
9191
+ const newVisualRows = computePromptVisualRows(after.buffer, composerRoom);
9192
+ const newLayout = computePromptLayout(
9193
+ newVisualRows,
9194
+ after,
9195
+ PICKER_COMPOSER_MAX_ROWS
9196
+ );
9197
+ if (newLayout.rendered !== composerRows) {
9198
+ renderFromScratch();
9341
9199
  return;
9342
9200
  }
9343
- const cur = this.currentLine();
9344
- const before = cur.slice(0, this.col);
9345
- const after = cur.slice(this.col);
9346
- const first = lines[0] ?? "";
9347
- const last = lines[lines.length - 1] ?? "";
9348
- const middle = lines.slice(1, -1);
9349
- this.setCurrentLine(before + first);
9350
- const newRows = [...middle, last + after];
9351
- this.buffer.splice(this.row + 1, 0, ...newRows);
9352
- this.row += lines.length - 1;
9353
- this.col = last.length;
9201
+ repaintComposerBody();
9202
+ return;
9354
9203
  }
9355
- insertNewline() {
9356
- const line = this.currentLine();
9357
- const before = line.slice(0, this.col);
9358
- const after = line.slice(this.col);
9359
- this.setCurrentLine(before);
9360
- this.buffer.splice(this.row + 1, 0, after);
9361
- this.row += 1;
9362
- this.col = 0;
9204
+ if (!searchActive && data?.isCharacter && name === "?") {
9205
+ mode = "help";
9206
+ renderHelp();
9207
+ return;
9363
9208
  }
9364
- backspace() {
9365
- if (this.col > 0) {
9366
- const line = this.currentLine();
9367
- this.setCurrentLine(line.slice(0, this.col - 1) + line.slice(this.col));
9368
- this.col -= 1;
9209
+ if (searchActive) {
9210
+ if (data?.isCharacter) {
9211
+ searchTerm += name;
9212
+ applyFilter();
9213
+ renderFromScratch();
9369
9214
  return;
9370
9215
  }
9371
- if (this.row === 0) {
9216
+ if (name === "BACKSPACE") {
9217
+ if (searchTerm.length > 0) {
9218
+ searchTerm = searchTerm.slice(0, -1);
9219
+ applyFilter();
9220
+ renderFromScratch();
9221
+ } else {
9222
+ searchActive = false;
9223
+ applyFilter();
9224
+ renderFromScratch();
9225
+ }
9226
+ return;
9227
+ }
9228
+ if (name === "ESCAPE" || name === "CTRL_C") {
9229
+ searchActive = false;
9230
+ searchTerm = "";
9231
+ applyFilter();
9232
+ renderFromScratch();
9372
9233
  return;
9373
9234
  }
9374
- const prev = this.buffer[this.row - 1] ?? "";
9375
- const cur = this.currentLine();
9376
- this.buffer.splice(this.row, 1);
9377
- this.row -= 1;
9378
- this.col = prev.length;
9379
- this.buffer[this.row] = prev + cur;
9380
9235
  }
9381
- deleteForward() {
9382
- const line = this.currentLine();
9383
- if (this.col < line.length) {
9384
- this.setCurrentLine(line.slice(0, this.col) + line.slice(this.col + 1));
9236
+ if (data?.isCharacter) {
9237
+ if (name === "/") {
9238
+ searchActive = true;
9239
+ searchTerm = "";
9240
+ applyFilter();
9241
+ renderFromScratch();
9385
9242
  return;
9386
9243
  }
9387
- if (this.row < this.buffer.length - 1) {
9388
- const next = this.buffer[this.row + 1] ?? "";
9389
- this.buffer.splice(this.row + 1, 1);
9390
- this.setCurrentLine(line + next);
9244
+ if (name === "n" || name === "N") {
9245
+ move(1);
9246
+ return;
9391
9247
  }
9392
- }
9393
- // ^U: kill from cursor to start of current line. At col 0 with a line
9394
- // above:
9395
- // - If the current line is empty, collapse it (kill just the
9396
- // newline) so the cursor lands at the end of the previous line.
9397
- // Don't slurp that line's contents.
9398
- // - Otherwise, kill the previous line entirely + the joining
9399
- // newline, so ^U from the start of a non-empty line walks up
9400
- // line-by-line.
9401
- // Single-line behavior is unchanged.
9402
- killLine() {
9403
- if (this.col > 0) {
9404
- const line = this.currentLine();
9405
- this.killBuffer = line.slice(0, this.col);
9406
- this.setCurrentLine(line.slice(this.col));
9407
- this.col = 0;
9248
+ if (name === "p" || name === "P") {
9249
+ move(-1);
9408
9250
  return;
9409
9251
  }
9410
- if (this.row === 0) {
9252
+ if (name === "c" || name === "C") {
9253
+ cleanup();
9254
+ resolve6({ kind: "new" });
9411
9255
  return;
9412
9256
  }
9413
- if (this.currentLine().length === 0) {
9414
- this.killBuffer = "\n";
9415
- this.buffer.splice(this.row, 1);
9416
- this.row -= 1;
9417
- this.col = this.currentLine().length;
9257
+ if (name === "q" || name === "Q") {
9258
+ cleanup();
9259
+ resolve6({ kind: "abort" });
9418
9260
  return;
9419
9261
  }
9420
- const prev = this.buffer[this.row - 1] ?? "";
9421
- this.killBuffer = prev + "\n";
9422
- this.buffer.splice(this.row - 1, 1);
9423
- this.row -= 1;
9424
- }
9425
- // ^K: kill from cursor to end of current line. At end-of-line with a
9426
- // line below:
9427
- // - If the current line is empty, collapse it (kill just the
9428
- // newline) so what was the next line takes its place. Don't slurp
9429
- // that line's contents.
9430
- // - Otherwise, kill the joining newline + the entire next line, so
9431
- // ^K from the end of a non-empty line walks down line-by-line.
9432
- // Single-line behavior is unchanged.
9433
- killToEnd() {
9434
- const line = this.currentLine();
9435
- if (this.col < line.length) {
9436
- this.killBuffer = line.slice(this.col);
9437
- this.setCurrentLine(line.slice(0, this.col));
9262
+ if (name === "o" || name === "O") {
9263
+ const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
9264
+ cwdOnly = !cwdOnly;
9265
+ applyFilter();
9266
+ if (keepId !== void 0) {
9267
+ const idx = visible.findIndex((s) => s.sessionId === keepId);
9268
+ if (idx >= 0) {
9269
+ selectedIdx = idx + 1;
9270
+ adjustScroll();
9271
+ }
9272
+ }
9273
+ renderFromScratch();
9438
9274
  return;
9439
9275
  }
9440
- if (this.row >= this.buffer.length - 1) {
9276
+ if (name === "h" || name === "H") {
9277
+ const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
9278
+ hostFilter = nextHostFilter(hostFilter, allSessions);
9279
+ applyFilter();
9280
+ if (keepId !== void 0) {
9281
+ const idx = visible.findIndex((s) => s.sessionId === keepId);
9282
+ if (idx >= 0) {
9283
+ selectedIdx = idx + 1;
9284
+ adjustScroll();
9285
+ }
9286
+ }
9287
+ renderFromScratch();
9441
9288
  return;
9442
9289
  }
9443
- if (line.length === 0) {
9444
- this.killBuffer = "\n";
9445
- this.buffer.splice(this.row, 1);
9290
+ if (name === "r" || name === "R") {
9291
+ const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
9292
+ void refresh(currentId);
9446
9293
  return;
9447
9294
  }
9448
- const next = this.buffer[this.row + 1] ?? "";
9449
- this.killBuffer = "\n" + next;
9450
- this.buffer.splice(this.row + 1, 1);
9451
- }
9452
- killWord() {
9453
- const line = this.currentLine();
9454
- if (this.col === 0) {
9455
- this.backspace();
9295
+ if ((name === "v" || name === "V") && selectedIdx > 0) {
9296
+ const session = visible[selectedIdx - 1];
9297
+ if (!session) {
9298
+ return;
9299
+ }
9300
+ cleanup();
9301
+ const result = {
9302
+ kind: "attach",
9303
+ sessionId: session.sessionId,
9304
+ readonly: true
9305
+ };
9306
+ if (session.agentId !== void 0) {
9307
+ result.agentId = session.agentId;
9308
+ }
9309
+ resolve6(result);
9456
9310
  return;
9457
9311
  }
9458
- let i = this.col;
9459
- while (i > 0 && /\s/.test(line[i - 1] ?? "")) {
9460
- i -= 1;
9312
+ if ((name === "k" || name === "K") && selectedIdx > 0) {
9313
+ const session = visible[selectedIdx - 1];
9314
+ if (!session) {
9315
+ return;
9316
+ }
9317
+ pendingAction = {
9318
+ sessionId: session.sessionId,
9319
+ cwd: session.cwd,
9320
+ status: session.status
9321
+ };
9322
+ mode = "confirm-kill";
9323
+ paintIndicator();
9324
+ return;
9461
9325
  }
9462
- while (i > 0 && !/\s/.test(line[i - 1] ?? "")) {
9463
- i -= 1;
9326
+ if (name === "t" && selectedIdx > 0) {
9327
+ const session = visible[selectedIdx - 1];
9328
+ if (!session) {
9329
+ return;
9330
+ }
9331
+ pendingAction = {
9332
+ sessionId: session.sessionId,
9333
+ cwd: session.cwd,
9334
+ status: session.status
9335
+ };
9336
+ renameBuffer = session.title ?? "";
9337
+ mode = "rename";
9338
+ paintIndicator();
9339
+ return;
9464
9340
  }
9465
- const killed = line.slice(i, this.col);
9466
- if (killed.length > 0) {
9467
- this.killBuffer = killed;
9341
+ if (name === "T" && selectedIdx > 0) {
9342
+ const session = visible[selectedIdx - 1];
9343
+ if (!session || session.status !== "live") {
9344
+ return;
9345
+ }
9346
+ void performRegen({ sessionId: session.sessionId });
9347
+ return;
9468
9348
  }
9469
- this.setCurrentLine(line.slice(0, i) + line.slice(this.col));
9470
- this.col = i;
9471
- }
9472
- yank() {
9473
- if (this.killBuffer.length === 0) {
9349
+ if ((name === "d" || name === "D") && selectedIdx > 0) {
9350
+ const session = visible[selectedIdx - 1];
9351
+ if (!session) {
9352
+ return;
9353
+ }
9354
+ if (session.status === "live") {
9355
+ transientStatus = "session is live \u2014 press k to kill it first";
9356
+ paintIndicator();
9357
+ return;
9358
+ }
9359
+ pendingAction = {
9360
+ sessionId: session.sessionId,
9361
+ cwd: session.cwd,
9362
+ status: session.status
9363
+ };
9364
+ mode = "confirm-delete";
9365
+ paintIndicator();
9474
9366
  return;
9475
9367
  }
9476
- this.insertText(this.killBuffer);
9368
+ return;
9477
9369
  }
9478
- moveLeft() {
9479
- if (this.col > 0) {
9480
- this.col -= 1;
9370
+ switch (name) {
9371
+ case "UP":
9372
+ case "SHIFT_TAB":
9373
+ move(-1);
9374
+ return;
9375
+ case "DOWN":
9376
+ case "TAB":
9377
+ move(1);
9378
+ return;
9379
+ case "PAGE_UP":
9380
+ move(-viewportSize);
9381
+ return;
9382
+ case "PAGE_DOWN":
9383
+ move(viewportSize);
9384
+ return;
9385
+ case "HOME":
9386
+ move(1 - selectedIdx);
9387
+ return;
9388
+ case "END":
9389
+ move(total);
9390
+ return;
9391
+ case "ENTER":
9392
+ case "KP_ENTER": {
9393
+ cleanup();
9394
+ if (selectedIdx === 0) {
9395
+ resolve6({ kind: "new" });
9396
+ return;
9397
+ }
9398
+ const session = visible[selectedIdx - 1];
9399
+ if (!session) {
9400
+ resolve6({ kind: "abort" });
9401
+ return;
9402
+ }
9403
+ const result = {
9404
+ kind: "attach",
9405
+ sessionId: session.sessionId
9406
+ };
9407
+ if (session.agentId !== void 0) {
9408
+ result.agentId = session.agentId;
9409
+ }
9410
+ resolve6(result);
9481
9411
  return;
9482
9412
  }
9483
- if (this.row > 0) {
9484
- this.row -= 1;
9485
- this.col = this.currentLine().length;
9486
- }
9413
+ case "ESCAPE":
9414
+ case "CTRL_C":
9415
+ case "CTRL_D":
9416
+ cleanup();
9417
+ resolve6({ kind: "abort" });
9418
+ return;
9419
+ }
9420
+ };
9421
+ term.grabInput({});
9422
+ term.on("key", onKey);
9423
+ term.on("resize", onResize);
9424
+ });
9425
+ }
9426
+ function readTermHeight(term) {
9427
+ return term.height ?? 24;
9428
+ }
9429
+ function readTermWidth(term) {
9430
+ return term.width ?? 80;
9431
+ }
9432
+ function formatComposerTitle(cwd, maxWidth) {
9433
+ const prefix = "Create new session in ";
9434
+ const budget = Math.max(1, maxWidth - prefix.length);
9435
+ return prefix + truncateMiddle(shortenHomePath(cwd), budget);
9436
+ }
9437
+ function filterByHost(sessions, hostFilter) {
9438
+ if (hostFilter === "__all") {
9439
+ return sessions;
9440
+ }
9441
+ if (hostFilter === "__local") {
9442
+ return sessions.filter(
9443
+ (s) => !s.importedFromMachine || !!s.upstreamSessionId
9444
+ );
9445
+ }
9446
+ return sessions.filter(
9447
+ (s) => s.importedFromMachine === hostFilter && !s.upstreamSessionId
9448
+ );
9449
+ }
9450
+ function nextHostFilter(current, sessions) {
9451
+ const hosts = /* @__PURE__ */ new Set();
9452
+ for (const s of sessions) {
9453
+ if (s.importedFromMachine && !s.upstreamSessionId) {
9454
+ hosts.add(s.importedFromMachine);
9455
+ }
9456
+ }
9457
+ const ordered = ["__local", ...[...hosts].sort(), "__all"];
9458
+ const idx = ordered.indexOf(current);
9459
+ if (idx === -1) {
9460
+ return "__local";
9461
+ }
9462
+ return ordered[(idx + 1) % ordered.length] ?? "__local";
9463
+ }
9464
+ function matchesSearch(s, term) {
9465
+ if (term.length === 0) {
9466
+ return true;
9467
+ }
9468
+ const t = term.toLowerCase();
9469
+ const haystacks = [
9470
+ stripHydraSessionPrefix(s.sessionId),
9471
+ s.upstreamSessionId ?? "",
9472
+ s.agentId ?? "",
9473
+ s.title ?? "",
9474
+ s.cwd,
9475
+ shortenHomePath(s.cwd)
9476
+ ];
9477
+ for (const h of haystacks) {
9478
+ if (h.toLowerCase().includes(t)) {
9479
+ return true;
9480
+ }
9481
+ }
9482
+ return false;
9483
+ }
9484
+ var ROW_PREFIX_WIDTH, PICKER_COMPOSER_MAX_ROWS, BOX_HORIZONTAL_PAD, HELP_KEYS_WIDTH, HELP_ENTRIES;
9485
+ var init_picker = __esm({
9486
+ "src/tui/picker.ts"() {
9487
+ "use strict";
9488
+ init_session_row();
9489
+ init_paths();
9490
+ init_session();
9491
+ init_discovery();
9492
+ init_input();
9493
+ init_screen();
9494
+ ROW_PREFIX_WIDTH = 2;
9495
+ PICKER_COMPOSER_MAX_ROWS = 4;
9496
+ BOX_HORIZONTAL_PAD = 4;
9497
+ HELP_KEYS_WIDTH = 20;
9498
+ HELP_ENTRIES = [
9499
+ ["Composer", "type prompt for new session; Enter creates + submits"],
9500
+ ["\u2193 from composer", "drop focus into session list"],
9501
+ null,
9502
+ ["\u2191 / \u2193 or n / p", "navigate sessions"],
9503
+ ["PgUp / PgDn", "page up / page down"],
9504
+ ["Home / End", "first / last"],
9505
+ ["Enter", "open selected session"],
9506
+ ["v", "view-only (open transcript without spawning the agent)"],
9507
+ null,
9508
+ ["/", "search sessions"],
9509
+ ["o", "toggle cwd-only filter"],
9510
+ ["h", "cycle host filter (local / <peer> / all)"],
9511
+ ["r", "refresh from daemon"],
9512
+ null,
9513
+ ["k", "kill the selected live session"],
9514
+ ["d", "delete the selected cold session"],
9515
+ ["t", "retitle the selected session"],
9516
+ ["T", "regenerate title via agent (live session)"],
9517
+ null,
9518
+ ["?", "toggle this help"],
9519
+ ["q / Esc / ^C / ^D", "quit picker (detach)"]
9520
+ ];
9521
+ }
9522
+ });
9523
+
9524
+ // src/core/cwd.ts
9525
+ import * as fs19 from "fs/promises";
9526
+ import * as path14 from "path";
9527
+ async function validateLocalCwd(input) {
9528
+ const trimmed = input.trim();
9529
+ if (trimmed.length === 0) {
9530
+ return { ok: false, reason: "path is empty" };
9531
+ }
9532
+ const resolved = path14.resolve(expandHome(trimmed));
9533
+ let stat5;
9534
+ try {
9535
+ stat5 = await fs19.stat(resolved);
9536
+ } catch {
9537
+ return { ok: false, reason: `${resolved} does not exist` };
9538
+ }
9539
+ if (!stat5.isDirectory()) {
9540
+ return { ok: false, reason: `${resolved} is not a directory` };
9541
+ }
9542
+ return { ok: true, path: resolved };
9543
+ }
9544
+ var init_cwd = __esm({
9545
+ "src/core/cwd.ts"() {
9546
+ "use strict";
9547
+ init_config();
9548
+ }
9549
+ });
9550
+
9551
+ // src/tui/prompt-utils.ts
9552
+ function resetTerminalModes() {
9553
+ process.stdout.write("\x1B[<u");
9554
+ process.stdout.write("\x1B[?2004l");
9555
+ process.stdout.write("\x1B[>4;0m");
9556
+ process.stdout.write("\x1B[>5;0m");
9557
+ process.stdout.write("\x1B[?1000l");
9558
+ process.stdout.write("\x1B[?1002l");
9559
+ process.stdout.write("\x1B[?1006l");
9560
+ process.stdout.write("\x1B[?1l");
9561
+ process.stdout.write("\x1B>");
9562
+ }
9563
+ function readTermWidth2(term) {
9564
+ return term.width ?? 80;
9565
+ }
9566
+ function readTermHeight2(term) {
9567
+ return term.height ?? 24;
9568
+ }
9569
+ function drawBox(term, opts) {
9570
+ const termW = readTermWidth2(term);
9571
+ const termH = readTermHeight2(term);
9572
+ const desiredContentW = opts.contentWidth ?? MAX_BOX_WIDTH;
9573
+ const maxContentW = Math.max(10, Math.min(MAX_BOX_WIDTH, termW - 4));
9574
+ const contentW = Math.min(desiredContentW, maxContentW);
9575
+ const w = contentW + 2;
9576
+ const contentH = Math.max(1, Math.min(opts.contentHeight, termH - 4));
9577
+ const h = contentH + 2;
9578
+ const x = Math.max(1, Math.floor((termW - w) / 2) + 1);
9579
+ const y = Math.max(1, Math.floor((termH - h) / 2) + 1);
9580
+ term.moveTo(1, 1).eraseDisplayBelow();
9581
+ const topInner = HORIZ.repeat(w - 2);
9582
+ const top = renderTitleStrip(topInner, opts.title);
9583
+ term.moveTo(x, y);
9584
+ term.dim.noFormat(TL);
9585
+ paintTopStrip(term, top);
9586
+ term.dim.noFormat(TR);
9587
+ for (let row = 1; row <= contentH; row++) {
9588
+ term.moveTo(x, y + row);
9589
+ term.dim.noFormat(VERT);
9590
+ term.moveTo(x + w - 1, y + row);
9591
+ term.dim.noFormat(VERT);
9592
+ }
9593
+ term.moveTo(x, y + h - 1);
9594
+ term.dim.noFormat(BL + HORIZ.repeat(w - 2) + BR);
9595
+ return {
9596
+ x,
9597
+ y,
9598
+ w,
9599
+ h,
9600
+ contentX: x + 1,
9601
+ contentY: y + 1,
9602
+ contentW,
9603
+ contentH
9604
+ };
9605
+ }
9606
+ function renderTitleStrip(innerDashes, title) {
9607
+ if (!title) {
9608
+ return { dashes: innerDashes };
9609
+ }
9610
+ const chip = ` ${title} `;
9611
+ if (chip.length + 4 > innerDashes.length) {
9612
+ return { dashes: innerDashes };
9613
+ }
9614
+ const offset = 2;
9615
+ const dashes = innerDashes.slice(0, offset) + " ".repeat(chip.length) + innerDashes.slice(offset + chip.length);
9616
+ return { dashes, title: { offset, text: chip } };
9617
+ }
9618
+ function paintTopStrip(term, strip) {
9619
+ if (!strip.title) {
9620
+ term.dim.noFormat(strip.dashes);
9621
+ return;
9622
+ }
9623
+ term.dim.noFormat(strip.dashes.slice(0, strip.title.offset));
9624
+ term.brightCyan.noFormat(strip.title.text);
9625
+ term.dim.noFormat(strip.dashes.slice(strip.title.offset + strip.title.text.length));
9626
+ }
9627
+ var MAX_BOX_WIDTH, HORIZ, VERT, TL, TR, BL, BR;
9628
+ var init_prompt_utils = __esm({
9629
+ "src/tui/prompt-utils.ts"() {
9630
+ "use strict";
9631
+ MAX_BOX_WIDTH = 64;
9632
+ HORIZ = "\u2500";
9633
+ VERT = "\u2502";
9634
+ TL = "\u250C";
9635
+ TR = "\u2510";
9636
+ BL = "\u2514";
9637
+ BR = "\u2518";
9638
+ }
9639
+ });
9640
+
9641
+ // src/tui/import-cwd-prompt.ts
9642
+ import * as os5 from "os";
9643
+ async function promptForImportCwd(term, session, opts = {}) {
9644
+ const defaultCwd = opts.defaultCwd ?? os5.homedir();
9645
+ resetTerminalModes();
9646
+ const shortId2 = stripHydraSessionPrefix(session.sessionId);
9647
+ const fromMachine = session.importedFromMachine ?? "another machine";
9648
+ const originalCwd = shortenHomePath(session.cwd);
9649
+ let buffer = defaultCwd;
9650
+ let errorLine = null;
9651
+ let busy = false;
9652
+ let layout = null;
9653
+ const render = () => {
9654
+ const contentHeight = 9;
9655
+ layout = drawBox(term, {
9656
+ contentHeight,
9657
+ title: "Run locally \u2014 choose cwd"
9658
+ });
9659
+ const innerW = layout.contentW;
9660
+ const headerRows = [
9661
+ { label: "session: ", value: shortId2 },
9662
+ { label: "from: ", value: fromMachine },
9663
+ { label: "cwd: ", value: originalCwd }
9664
+ ];
9665
+ let row = 0;
9666
+ for (const hr of headerRows) {
9667
+ term.moveTo(layout.contentX, layout.contentY + row);
9668
+ term.dim.noFormat(` ${hr.label}`);
9669
+ term.noFormat(truncate2(hr.value, innerW - hr.label.length - 2));
9670
+ row++;
9671
+ }
9672
+ row++;
9673
+ term.moveTo(layout.contentX, layout.contentY + row);
9674
+ term.noFormat(" Pick a local cwd for this session:");
9675
+ row += 2;
9676
+ paintInputRow(row);
9677
+ row += 2;
9678
+ if (errorLine !== null) {
9679
+ term.moveTo(layout.contentX, layout.contentY + row);
9680
+ term.red.noFormat(` ${truncate2(errorLine, innerW - 2)}`);
9681
+ } else {
9682
+ term.moveTo(layout.contentX, layout.contentY + row);
9683
+ term.dim.noFormat(
9684
+ " Enter accept \xB7 Esc back \xB7 ^U clear \xB7 ^W kill word"
9685
+ );
9686
+ }
9687
+ };
9688
+ const inputRow = () => 7;
9689
+ const paintInputRow = (rowOffset) => {
9690
+ if (!layout) {
9691
+ return;
9692
+ }
9693
+ const r = rowOffset ?? inputRow();
9694
+ term.moveTo(layout.contentX, layout.contentY + r).eraseLineAfter();
9695
+ term.moveTo(layout.x + layout.w - 1, layout.contentY + r);
9696
+ term.dim.noFormat("\u2502");
9697
+ term.moveTo(layout.contentX, layout.contentY + r);
9698
+ term.bold.noFormat(" cwd: ");
9699
+ const available = layout.contentW - " cwd: ".length - 2;
9700
+ term.noFormat(truncateLeft(buffer, available));
9701
+ if (!busy) {
9702
+ term.bgWhite(" ");
9703
+ }
9704
+ };
9705
+ const repaintInput = () => {
9706
+ paintInputRow();
9707
+ if (!layout) {
9708
+ return;
9709
+ }
9710
+ const errRow = inputRow() + 2;
9711
+ term.moveTo(layout.contentX, layout.contentY + errRow).eraseLineAfter();
9712
+ term.moveTo(layout.x + layout.w - 1, layout.contentY + errRow);
9713
+ term.dim.noFormat("\u2502");
9714
+ term.moveTo(layout.contentX, layout.contentY + errRow);
9715
+ if (errorLine !== null) {
9716
+ term.red.noFormat(` ${truncate2(errorLine, layout.contentW - 2)}`);
9717
+ } else {
9718
+ term.dim.noFormat(
9719
+ " Enter accept \xB7 Esc back \xB7 ^U clear \xB7 ^W kill word"
9720
+ );
9721
+ }
9722
+ };
9723
+ render();
9724
+ return await new Promise((resolve6) => {
9725
+ let resolved = false;
9726
+ const cleanup = () => {
9727
+ if (resolved) {
9728
+ return;
9487
9729
  }
9488
- moveRight() {
9489
- if (this.col < this.currentLine().length) {
9490
- this.col += 1;
9491
- return;
9492
- }
9493
- if (this.row < this.buffer.length - 1) {
9494
- this.row += 1;
9495
- this.col = 0;
9496
- }
9730
+ resolved = true;
9731
+ term.off("key", onKey);
9732
+ term.off("resize", onResize);
9733
+ term.grabInput(false);
9734
+ term.hideCursor(false);
9735
+ term.moveTo(1, 1).eraseDisplayBelow();
9736
+ };
9737
+ const finish = (value) => {
9738
+ cleanup();
9739
+ resolve6(value);
9740
+ };
9741
+ const onResize = () => {
9742
+ if (resolved) {
9743
+ return;
9497
9744
  }
9498
- moveWordBackward() {
9499
- if (this.col === 0) {
9500
- if (this.row === 0) {
9501
- return;
9502
- }
9503
- this.row -= 1;
9504
- this.col = this.currentLine().length;
9505
- return;
9506
- }
9507
- const line = this.currentLine();
9508
- let i = this.col;
9509
- while (i > 0 && /\s/.test(line[i - 1] ?? "")) {
9510
- i -= 1;
9511
- }
9512
- while (i > 0 && !/\s/.test(line[i - 1] ?? "")) {
9513
- i -= 1;
9514
- }
9515
- this.col = i;
9745
+ render();
9746
+ };
9747
+ const onKey = (name, _matches, data) => {
9748
+ if (busy) {
9749
+ return;
9516
9750
  }
9517
- moveWordForward() {
9518
- const line = this.currentLine();
9519
- if (this.col >= line.length) {
9520
- if (this.row >= this.buffer.length - 1) {
9751
+ if (name === "ENTER" || name === "KP_ENTER") {
9752
+ const candidate = buffer;
9753
+ busy = true;
9754
+ errorLine = null;
9755
+ repaintInput();
9756
+ void validateLocalCwd(candidate).then((result) => {
9757
+ busy = false;
9758
+ if (result.ok) {
9759
+ finish({ kind: "ok", path: result.path });
9521
9760
  return;
9522
9761
  }
9523
- this.row += 1;
9524
- this.col = 0;
9525
- return;
9526
- }
9527
- let i = this.col;
9528
- while (i < line.length && /\s/.test(line[i] ?? "")) {
9529
- i += 1;
9530
- }
9531
- while (i < line.length && !/\s/.test(line[i] ?? "")) {
9532
- i += 1;
9533
- }
9534
- this.col = i;
9535
- }
9536
- // Up walks the navigation stack from newest to oldest: pending queue
9537
- // items first (so the user can edit something they just enqueued),
9538
- // then prompt history. Cursor movement within a multi-line buffer
9539
- // takes priority when not already navigating.
9540
- handleUp() {
9541
- if (this.row > 0) {
9542
- this.row -= 1;
9543
- this.col = Math.min(this.col, this.currentLine().length);
9544
- return [];
9545
- }
9546
- if (this.queueIndex === -1 && this.historyIndex === -1) {
9547
- if (this.queue.length === 0 && this.history.length === 0) {
9548
- return [];
9549
- }
9550
- this.savedDraft = {
9551
- buffer: [...this.buffer],
9552
- row: this.row,
9553
- col: this.col
9554
- };
9555
- this.savedAttachments = [...this.attachments];
9556
- this.attachments = [];
9557
- if (this.queue.length > 0) {
9558
- this.queueIndex = this.queue.length - 1;
9559
- this.loadEntry(this.queue[this.queueIndex] ?? "");
9560
- } else {
9561
- this.historyIndex = this.history.length - 1;
9562
- this.loadEntry(this.history[this.historyIndex] ?? "");
9563
- }
9564
- return [];
9565
- }
9566
- if (this.queueIndex >= 0) {
9567
- if (this.queueIndex > 0) {
9568
- this.queueIndex -= 1;
9569
- this.loadEntry(this.queue[this.queueIndex] ?? "");
9570
- return [];
9571
- }
9572
- if (this.history.length === 0) {
9573
- return [];
9574
- }
9575
- this.queueIndex = -1;
9576
- this.historyIndex = this.history.length - 1;
9577
- this.loadEntry(this.history[this.historyIndex] ?? "");
9578
- return [];
9579
- }
9580
- if (this.historyIndex > 0) {
9581
- this.historyIndex -= 1;
9582
- this.loadEntry(this.history[this.historyIndex] ?? "");
9583
- }
9584
- return [];
9585
- }
9586
- // Down reverses the Up walk: history (older → newer), then queue
9587
- // (oldest → newest), then restore the original draft. Within a
9588
- // multi-line buffer, plain cursor movement still wins when no
9589
- // navigation is in progress.
9590
- handleDown() {
9591
- if (this.row < this.buffer.length - 1 && this.historyIndex === -1 && this.queueIndex === -1) {
9592
- this.row += 1;
9593
- this.col = Math.min(this.col, this.currentLine().length);
9594
- return [];
9595
- }
9596
- if (this.historyIndex >= 0) {
9597
- if (this.historyIndex < this.history.length - 1) {
9598
- this.historyIndex += 1;
9599
- this.loadEntry(this.history[this.historyIndex] ?? "");
9600
- return [];
9601
- }
9602
- this.historyIndex = -1;
9603
- if (this.queue.length > 0) {
9604
- this.queueIndex = 0;
9605
- this.loadEntry(this.queue[this.queueIndex] ?? "");
9606
- return [];
9607
- }
9608
- this.restoreDraft();
9609
- return [];
9610
- }
9611
- if (this.queueIndex >= 0) {
9612
- if (this.queueIndex < this.queue.length - 1) {
9613
- this.queueIndex += 1;
9614
- this.loadEntry(this.queue[this.queueIndex] ?? "");
9615
- return [];
9616
- }
9617
- this.queueIndex = -1;
9618
- this.restoreDraft();
9619
- return [];
9620
- }
9621
- return [];
9762
+ errorLine = result.reason;
9763
+ repaintInput();
9764
+ });
9765
+ return;
9622
9766
  }
9623
- restoreDraft() {
9624
- if (this.savedDraft) {
9625
- this.buffer = [...this.savedDraft.buffer];
9626
- this.row = this.savedDraft.row;
9627
- this.col = this.savedDraft.col;
9628
- this.savedDraft = null;
9629
- this.attachments = this.savedAttachments ?? [];
9630
- this.savedAttachments = null;
9631
- } else {
9632
- this.clearBuffer();
9633
- }
9767
+ if (name === "ESCAPE") {
9768
+ finish({ kind: "back" });
9769
+ return;
9634
9770
  }
9635
- // Engage reverse-incremental search over prompt history. Uses the
9636
- // current buffer text as the search query. With an empty buffer we
9637
- // enter search mode in an "empty query, no match shown" state — the
9638
- // banner indicator lights up, and as the user types we extend the
9639
- // query and load top matches. We deliberately do NOT auto-load the
9640
- // most recent entry on an empty ^R (that's a surprise — Up-arrow
9641
- // already walks history if that's what they wanted). With a
9642
- // non-empty query that has no history match, escalate straight to
9643
- // scrollback search so the typed term searches session output.
9644
- startHistorySearch() {
9645
- const query = this.bufferText().toLowerCase();
9646
- if (query.length === 0) {
9647
- this.historySearch = {
9648
- query: "",
9649
- matchIndices: [],
9650
- cursor: 0,
9651
- savedDraft: {
9652
- buffer: [...this.buffer],
9653
- row: this.row,
9654
- col: this.col
9655
- }
9656
- };
9657
- return [];
9658
- }
9659
- const matchIndices = this.findHistoryMatches(query);
9660
- if (matchIndices.length === 0) {
9661
- return [{ type: "escalate-search", query }];
9662
- }
9663
- this.historySearch = {
9664
- query,
9665
- matchIndices,
9666
- cursor: 0,
9667
- savedDraft: {
9668
- buffer: [...this.buffer],
9669
- row: this.row,
9670
- col: this.col
9671
- }
9672
- };
9673
- this.loadEntry(this.history[matchIndices[0]] ?? "");
9674
- return [];
9771
+ if (name === "CTRL_C" || name === "CTRL_D") {
9772
+ finish({ kind: "cancel" });
9773
+ return;
9675
9774
  }
9676
- // ^R advance. At the oldest match with a non-empty query, falls
9677
- // through to scrollback search (same escalate path as a never-
9678
- // matched startHistorySearch). With an empty query at the oldest
9679
- // match (i.e. the user walked all history with no filter), advance
9680
- // is a no-op so the buffer stays on the oldest entry.
9681
- advanceHistorySearch() {
9682
- if (this.historySearch === null) {
9683
- return [];
9684
- }
9685
- const search = this.historySearch;
9686
- const atOldest = search.cursor >= search.matchIndices.length - 1;
9687
- if (atOldest) {
9688
- if (search.query.length === 0) {
9689
- return [];
9690
- }
9691
- const query = search.query;
9692
- const draft = search.savedDraft;
9693
- this.historySearch = null;
9694
- this.buffer = [...draft.buffer];
9695
- this.row = draft.row;
9696
- this.col = draft.col;
9697
- return [{ type: "escalate-search", query }];
9775
+ if (name === "BACKSPACE") {
9776
+ if (buffer.length > 0) {
9777
+ buffer = buffer.slice(0, -1);
9778
+ errorLine = null;
9779
+ repaintInput();
9698
9780
  }
9699
- search.cursor += 1;
9700
- const idx = search.matchIndices[search.cursor];
9701
- this.loadEntry(this.history[idx] ?? "");
9702
- return [];
9781
+ return;
9703
9782
  }
9704
- // ^S retreat walk toward newer matches. No-op at the newest match
9705
- // (no wrap, mirroring ^R no-wrap at the oldest).
9706
- retreatHistorySearch() {
9707
- if (this.historySearch === null) {
9708
- return;
9709
- }
9710
- if (this.historySearch.cursor === 0) {
9711
- return;
9712
- }
9713
- this.historySearch.cursor -= 1;
9714
- const idx = this.historySearch.matchIndices[this.historySearch.cursor];
9715
- this.loadEntry(this.history[idx] ?? "");
9783
+ if (name === "CTRL_U") {
9784
+ buffer = "";
9785
+ errorLine = null;
9786
+ repaintInput();
9787
+ return;
9716
9788
  }
9717
- // Backspace / typing within search mode mutates the query and
9718
- // re-searches. When the new query is empty, restore the saved
9719
- // draft buffer (typically empty) and stay in search mode — the
9720
- // user can keep typing. When the new query has matches, load the
9721
- // top one. When the new query has no matches, escalate to scrollback
9722
- // search so the typed term applies there instead.
9723
- mutateHistorySearchQuery(newQuery) {
9724
- if (this.historySearch === null) {
9725
- return [];
9726
- }
9727
- if (newQuery.length === 0) {
9728
- this.historySearch.query = "";
9729
- this.historySearch.matchIndices = [];
9730
- this.historySearch.cursor = 0;
9731
- const draft = this.historySearch.savedDraft;
9732
- this.buffer = [...draft.buffer];
9733
- this.row = draft.row;
9734
- this.col = draft.col;
9735
- return [];
9736
- }
9737
- const matchIndices = this.findHistoryMatches(newQuery);
9738
- if (matchIndices.length === 0) {
9739
- const draft = this.historySearch.savedDraft;
9740
- this.historySearch = null;
9741
- this.buffer = [...draft.buffer];
9742
- this.row = draft.row;
9743
- this.col = draft.col;
9744
- return [{ type: "escalate-search", query: newQuery }];
9745
- }
9746
- this.historySearch.query = newQuery;
9747
- this.historySearch.matchIndices = matchIndices;
9748
- this.historySearch.cursor = 0;
9749
- this.loadEntry(this.history[matchIndices[0]] ?? "");
9750
- return [];
9789
+ if (name === "CTRL_W") {
9790
+ const trimmedRight = buffer.replace(/[/\s]+$/, "");
9791
+ const lastSep = Math.max(
9792
+ trimmedRight.lastIndexOf("/"),
9793
+ trimmedRight.lastIndexOf(" ")
9794
+ );
9795
+ buffer = lastSep >= 0 ? trimmedRight.slice(0, lastSep + 1) : "";
9796
+ errorLine = null;
9797
+ repaintInput();
9798
+ return;
9799
+ }
9800
+ if (data?.isCharacter) {
9801
+ buffer += name;
9802
+ errorLine = null;
9803
+ repaintInput();
9804
+ return;
9805
+ }
9806
+ };
9807
+ term.grabInput({});
9808
+ term.on("key", onKey);
9809
+ term.on("resize", onResize);
9810
+ });
9811
+ }
9812
+ function truncate2(s, max) {
9813
+ if (max <= 1) {
9814
+ return "";
9815
+ }
9816
+ if (s.length <= max) {
9817
+ return s;
9818
+ }
9819
+ return s.slice(0, Math.max(0, max - 1)) + "\u2026";
9820
+ }
9821
+ function truncateLeft(s, max) {
9822
+ if (max <= 1) {
9823
+ return "";
9824
+ }
9825
+ if (s.length <= max) {
9826
+ return s;
9827
+ }
9828
+ return "\u2026" + s.slice(s.length - (max - 1));
9829
+ }
9830
+ var init_import_cwd_prompt = __esm({
9831
+ "src/tui/import-cwd-prompt.ts"() {
9832
+ "use strict";
9833
+ init_paths();
9834
+ init_session();
9835
+ init_cwd();
9836
+ init_prompt_utils();
9837
+ }
9838
+ });
9839
+
9840
+ // src/tui/import-action-prompt.ts
9841
+ function actionPromptStep(selected, key, choices = ACTION_CHOICES) {
9842
+ if (key.kind === "cancel") {
9843
+ return { kind: "cancel" };
9844
+ }
9845
+ if (key.kind === "back") {
9846
+ return { kind: "back" };
9847
+ }
9848
+ if (key.kind === "enter") {
9849
+ const choice = choices[selected];
9850
+ if (!choice) {
9851
+ return { kind: "back" };
9852
+ }
9853
+ return { kind: "resolve", action: choice.key };
9854
+ }
9855
+ if (key.kind === "up") {
9856
+ return {
9857
+ kind: "continue",
9858
+ selected: Math.max(0, selected - 1)
9859
+ };
9860
+ }
9861
+ if (key.kind === "down") {
9862
+ return {
9863
+ kind: "continue",
9864
+ selected: Math.min(choices.length - 1, selected + 1)
9865
+ };
9866
+ }
9867
+ if (key.kind === "char") {
9868
+ const lower = key.ch.toLowerCase();
9869
+ if (lower === "n") {
9870
+ return {
9871
+ kind: "continue",
9872
+ selected: Math.min(choices.length - 1, selected + 1)
9873
+ };
9874
+ }
9875
+ if (lower === "p") {
9876
+ return {
9877
+ kind: "continue",
9878
+ selected: Math.max(0, selected - 1)
9879
+ };
9880
+ }
9881
+ const idx = choices.findIndex((c) => c.hotkey.toLowerCase() === lower);
9882
+ if (idx >= 0) {
9883
+ const choice = choices[idx];
9884
+ if (choice) {
9885
+ return { kind: "resolve", action: choice.key };
9886
+ }
9887
+ }
9888
+ }
9889
+ return { kind: "continue", selected };
9890
+ }
9891
+ async function promptForImportAction(term, session) {
9892
+ resetTerminalModes();
9893
+ const shortId2 = stripHydraSessionPrefix(session.sessionId);
9894
+ const fromMachine = session.importedFromMachine ?? "another machine";
9895
+ const originalCwd = shortenHomePath(session.cwd);
9896
+ let selected = 0;
9897
+ const render = () => {
9898
+ const choiceRows = ACTION_CHOICES.length * 2;
9899
+ const contentHeight = 7 + choiceRows + 2;
9900
+ const layout = drawBox(term, {
9901
+ contentHeight,
9902
+ title: "Imported session"
9903
+ });
9904
+ const innerW = layout.contentW;
9905
+ const headerRows = [
9906
+ { label: "session: ", value: shortId2 },
9907
+ { label: "from: ", value: fromMachine },
9908
+ { label: "cwd: ", value: originalCwd }
9909
+ ];
9910
+ let row = 0;
9911
+ for (const hr of headerRows) {
9912
+ term.moveTo(layout.contentX, layout.contentY + row);
9913
+ term.dim.noFormat(` ${hr.label}`);
9914
+ term.noFormat(truncate3(hr.value, innerW - hr.label.length - 2));
9915
+ row++;
9916
+ }
9917
+ row++;
9918
+ term.moveTo(layout.contentX, layout.contentY + row);
9919
+ term.noFormat(" What do you want to do?");
9920
+ row += 2;
9921
+ for (let i = 0; i < ACTION_CHOICES.length; i++) {
9922
+ const choice = ACTION_CHOICES[i];
9923
+ if (!choice) {
9924
+ continue;
9751
9925
  }
9752
- findHistoryMatches(query) {
9753
- const out = [];
9754
- for (let i = this.history.length - 1; i >= 0; i--) {
9755
- const entry = this.history[i] ?? "";
9756
- if (query.length === 0 || entry.toLowerCase().includes(query)) {
9757
- out.push(i);
9758
- }
9759
- }
9760
- return out;
9926
+ const pointer = i === selected ? "\u276F" : " ";
9927
+ const label = ` ${pointer} ${choice.label}`;
9928
+ term.moveTo(layout.contentX, layout.contentY + row);
9929
+ if (i === selected) {
9930
+ term.brightWhite.bgBlue.noFormat(padRight(label, innerW));
9931
+ } else {
9932
+ term.noFormat(label);
9761
9933
  }
9762
- cancelHistorySearch() {
9763
- if (this.historySearch === null) {
9764
- return;
9765
- }
9766
- const draft = this.historySearch.savedDraft;
9767
- this.historySearch = null;
9768
- this.buffer = [...draft.buffer];
9769
- this.row = draft.row;
9770
- this.col = draft.col;
9934
+ row++;
9935
+ term.moveTo(layout.contentX, layout.contentY + row);
9936
+ term.dim.noFormat(` ${choice.description}`);
9937
+ row++;
9938
+ }
9939
+ row++;
9940
+ term.moveTo(layout.contentX, layout.contentY + row);
9941
+ term.dim.noFormat(" \u2191/\u2193 navigate \xB7 Enter select \xB7 r/v jump \xB7 Esc back");
9942
+ return layout;
9943
+ };
9944
+ render();
9945
+ term.hideCursor();
9946
+ return await new Promise((resolve6) => {
9947
+ let resolved = false;
9948
+ const cleanup = () => {
9949
+ if (resolved) {
9950
+ return;
9771
9951
  }
9772
- loadEntry(text) {
9773
- this.buffer = text.split("\n");
9774
- if (this.buffer.length === 0) {
9775
- this.buffer = [""];
9776
- }
9777
- this.row = this.buffer.length - 1;
9778
- this.col = (this.buffer[this.row] ?? "").length;
9952
+ resolved = true;
9953
+ term.off("key", onKey);
9954
+ term.off("resize", onResize);
9955
+ term.grabInput(false);
9956
+ term.hideCursor(false);
9957
+ term.moveTo(1, 1).eraseDisplayBelow();
9958
+ };
9959
+ const finish = (value) => {
9960
+ cleanup();
9961
+ resolve6(value);
9962
+ };
9963
+ const onResize = () => {
9964
+ if (resolved) {
9965
+ return;
9779
9966
  }
9780
- send() {
9781
- const text = this.bufferText();
9782
- if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
9783
- const index = this.queueIndex;
9784
- const attachments2 = [...this.attachments];
9785
- this.clearBuffer();
9786
- if (text.trim().length === 0) {
9787
- return [{ type: "queue-remove", index }];
9788
- }
9789
- return [{ type: "queue-edit", index, text, attachments: attachments2 }];
9790
- }
9791
- if (text.trim().length === 0 && this.attachments.length === 0) {
9792
- return [];
9793
- }
9794
- const planMode = this.planMode;
9795
- const attachments = [...this.attachments];
9796
- this.clearBuffer();
9797
- return [{ type: "send", text, planMode, attachments }];
9967
+ render();
9968
+ };
9969
+ const onKey = (name, _matches, data) => {
9970
+ const input = mapKey(name, data);
9971
+ if (!input) {
9972
+ return;
9798
9973
  }
9799
- // Shift+Enter: amend the in-flight turn. Editing a queued slot
9800
- // delegates to the existing queue-edit / queue-remove path — Shift+Enter
9801
- // there has no special meaning since the entry is already queued (not
9802
- // running). With an empty draft and no attachments we emit nothing
9803
- // (no-op). Otherwise emit an "amend" effect; the app decides whether
9804
- // to route through amend_prompt or fall through to a regular send.
9805
- amend() {
9806
- const text = this.bufferText();
9807
- if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
9808
- const index = this.queueIndex;
9809
- const attachments2 = [...this.attachments];
9810
- this.clearBuffer();
9811
- if (text.trim().length === 0) {
9812
- return [{ type: "queue-remove", index }];
9813
- }
9814
- return [{ type: "queue-edit", index, text, attachments: attachments2 }];
9815
- }
9816
- if (text.trim().length === 0 && this.attachments.length === 0) {
9817
- return [];
9818
- }
9819
- const planMode = this.planMode;
9820
- const attachments = [...this.attachments];
9821
- this.clearBuffer();
9822
- return [{ type: "amend", text, planMode, attachments }];
9974
+ const step = actionPromptStep(selected, input);
9975
+ if (step.kind === "cancel") {
9976
+ finish("cancel");
9977
+ return;
9823
9978
  }
9824
- // Home: jump to the very start of the prompt buffer. If we're already
9825
- // there, fall through to scrolling the scrollback to its top.
9826
- handleHome() {
9827
- if (this.row !== 0 || this.col !== 0) {
9828
- this.row = 0;
9829
- this.col = 0;
9830
- return [];
9831
- }
9832
- return [{ type: "scroll-to-top" }];
9979
+ if (step.kind === "back") {
9980
+ finish("back");
9981
+ return;
9833
9982
  }
9834
- // End: jump to the end of the last line of the prompt buffer. Already
9835
- // there → scroll the scrollback to the bottom (newest).
9836
- handleEnd() {
9837
- const lastRow = this.buffer.length - 1;
9838
- const lastCol = (this.buffer[lastRow] ?? "").length;
9839
- if (this.row !== lastRow || this.col !== lastCol) {
9840
- this.row = lastRow;
9841
- this.col = lastCol;
9842
- return [];
9843
- }
9844
- return [{ type: "scroll-to-bottom" }];
9983
+ if (step.kind === "resolve") {
9984
+ finish(step.action);
9985
+ return;
9845
9986
  }
9846
- handleCtrlC() {
9847
- if (this.queueIndex >= 0) {
9848
- const index = this.queueIndex;
9849
- this.queueIndex = -1;
9850
- this.restoreDraft();
9851
- return [{ type: "queue-remove", index }];
9852
- }
9853
- if (!this.bufferIsEmpty() || this.attachments.length > 0) {
9854
- this.buffer = [""];
9855
- this.row = 0;
9856
- this.col = 0;
9857
- this.attachments = [];
9858
- this.historyIndex = -1;
9859
- this.savedDraft = null;
9860
- this.savedAttachments = null;
9861
- return [];
9862
- }
9863
- if (this.turnRunning) {
9864
- return [{ type: "cancel" }];
9865
- }
9866
- return [{ type: "exit" }];
9987
+ if (step.selected !== selected) {
9988
+ selected = step.selected;
9989
+ render();
9867
9990
  }
9868
9991
  };
9992
+ term.grabInput({});
9993
+ term.on("key", onKey);
9994
+ term.on("resize", onResize);
9995
+ });
9996
+ }
9997
+ function mapKey(name, data) {
9998
+ if (name === "UP") {
9999
+ return { kind: "up" };
10000
+ }
10001
+ if (name === "DOWN") {
10002
+ return { kind: "down" };
10003
+ }
10004
+ if (name === "ENTER" || name === "KP_ENTER") {
10005
+ return { kind: "enter" };
10006
+ }
10007
+ if (name === "ESCAPE") {
10008
+ return { kind: "back" };
10009
+ }
10010
+ if (name === "CTRL_C" || name === "CTRL_D") {
10011
+ return { kind: "cancel" };
10012
+ }
10013
+ if (data?.isCharacter) {
10014
+ return { kind: "char", ch: name };
10015
+ }
10016
+ return null;
10017
+ }
10018
+ function truncate3(s, max) {
10019
+ if (max <= 1) {
10020
+ return "";
10021
+ }
10022
+ if (s.length <= max) {
10023
+ return s;
10024
+ }
10025
+ return s.slice(0, Math.max(0, max - 1)) + "\u2026";
10026
+ }
10027
+ function padRight(s, w) {
10028
+ if (s.length >= w) {
10029
+ return s.slice(0, w);
10030
+ }
10031
+ return s + " ".repeat(w - s.length);
10032
+ }
10033
+ var ACTION_CHOICES;
10034
+ var init_import_action_prompt = __esm({
10035
+ "src/tui/import-action-prompt.ts"() {
10036
+ "use strict";
10037
+ init_paths();
10038
+ init_session();
10039
+ init_prompt_utils();
10040
+ ACTION_CHOICES = [
10041
+ {
10042
+ key: "run-local",
10043
+ label: "Run locally",
10044
+ hotkey: "r",
10045
+ description: "spawn the agent on this machine with a local cwd"
10046
+ },
10047
+ {
10048
+ key: "view",
10049
+ label: "View transcript",
10050
+ hotkey: "v",
10051
+ description: "open read-only, no agent spawn"
10052
+ }
10053
+ ];
9869
10054
  }
9870
10055
  });
9871
10056
 
@@ -11624,7 +11809,16 @@ async function runSession(term, config, target, opts, exitHint) {
11624
11809
  if (choice.kind === "new") {
11625
11810
  const { sessionId: _drop, ...rest } = opts;
11626
11811
  void _drop;
11627
- resume({ ...rest, cwd: resolvedCwd, forceNew: true, readonly: false });
11812
+ const nextOpts2 = {
11813
+ ...rest,
11814
+ cwd: resolvedCwd,
11815
+ forceNew: true,
11816
+ readonly: false
11817
+ };
11818
+ if (choice.prompt !== void 0) {
11819
+ nextOpts2.initialPrompt = choice.prompt;
11820
+ }
11821
+ resume(nextOpts2);
11628
11822
  return;
11629
11823
  }
11630
11824
  if (choice.kind !== "attach") {
@@ -12685,6 +12879,9 @@ connection lost: ${err.message}
12685
12879
  stop(err ? 1 : 0);
12686
12880
  });
12687
12881
  process.on("SIGINT", sigintHandler);
12882
+ if (opts.initialPrompt && ctx.sessionId === "__new__") {
12883
+ enqueuePrompt(opts.initialPrompt, []);
12884
+ }
12688
12885
  return await sessionDone;
12689
12886
  }
12690
12887
  async function resolveSession(term, config, target, opts) {
@@ -12719,9 +12916,6 @@ async function resolveSession(term, config, target, opts) {
12719
12916
  }
12720
12917
  while (true) {
12721
12918
  const sessions = await listSessions(target);
12722
- if (sessions.length === 0) {
12723
- return newCtx(opts, cwd, config);
12724
- }
12725
12919
  const choice = await pickSession(term, {
12726
12920
  cwd,
12727
12921
  sessions,
@@ -12732,6 +12926,9 @@ async function resolveSession(term, config, target, opts) {
12732
12926
  return null;
12733
12927
  }
12734
12928
  if (choice.kind === "new") {
12929
+ if (choice.prompt !== void 0) {
12930
+ opts.initialPrompt = choice.prompt;
12931
+ }
12735
12932
  return newCtx(opts, cwd, config);
12736
12933
  }
12737
12934
  opts.readonly = choice.readonly === true;