agent-sh 0.12.26 → 0.12.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-loop.js +2 -2
- package/dist/agent/conversation-state.js +22 -1
- package/dist/install.js +84 -4
- package/dist/utils/floating-panel.d.ts +16 -4
- package/dist/utils/floating-panel.js +209 -66
- package/examples/extensions/emacs-buffer.ts +364 -0
- package/examples/extensions/overlay-agent.ts +28 -5
- package/examples/extensions/terminal-buffer.ts +174 -33
- package/examples/extensions/tunnel-vision.ts +405 -0
- package/examples/extensions/web-access.ts +3 -108
- package/package.json +1 -1
|
@@ -90,7 +90,8 @@ export class FloatingPanel {
|
|
|
90
90
|
* - `{prefix}:render-border-bottom(ctx: FrameContext) -> string`
|
|
91
91
|
* - `{prefix}:composite-row(content: string, bgLine: string|null, boxLeft: number, boxW: number, cols: number) -> string`
|
|
92
92
|
* - `{prefix}:submit(query: string) -> void`
|
|
93
|
-
* - `{prefix}:
|
|
93
|
+
* - `{prefix}:hide() -> void` (screen down; conversation state preserved)
|
|
94
|
+
* - `{prefix}:reset() -> void` (conversation state cleared)
|
|
94
95
|
* - `{prefix}:show() -> void`
|
|
95
96
|
* - `{prefix}:input(data: string) -> boolean`
|
|
96
97
|
* - `{prefix}:build-row(content: string, width: number) -> string`
|
|
@@ -123,6 +124,10 @@ export class FloatingPanel {
|
|
|
123
124
|
wrapCacheWidth = 0;
|
|
124
125
|
passthroughTimer = null;
|
|
125
126
|
prevSerialized = "";
|
|
127
|
+
// ── Autocomplete ────────────────────────────────────────────
|
|
128
|
+
autocompleteItems = [];
|
|
129
|
+
autocompleteIndex = 0;
|
|
130
|
+
autocompleteActive = false;
|
|
126
131
|
constructor(bus, config, handlers) {
|
|
127
132
|
this.bus = bus;
|
|
128
133
|
this.surface = config.surface ?? new StdoutSurface();
|
|
@@ -166,10 +171,54 @@ export class FloatingPanel {
|
|
|
166
171
|
}
|
|
167
172
|
all.push(...wrapped);
|
|
168
173
|
}
|
|
169
|
-
|
|
174
|
+
if (ctx.phase === "input" && this.autocompleteActive && this.autocompleteItems.length > 0) {
|
|
175
|
+
const ACMax = 5;
|
|
176
|
+
const items = this.autocompleteItems;
|
|
177
|
+
let acStart = 0;
|
|
178
|
+
let acEnd = items.length;
|
|
179
|
+
if (items.length > ACMax) {
|
|
180
|
+
acStart = Math.max(0, this.autocompleteIndex - Math.floor(ACMax / 2));
|
|
181
|
+
acStart = Math.min(acStart, items.length - ACMax);
|
|
182
|
+
acEnd = acStart + ACMax;
|
|
183
|
+
}
|
|
184
|
+
for (let i = acStart; i < acEnd; i++) {
|
|
185
|
+
const item = items[i];
|
|
186
|
+
const selected = i === this.autocompleteIndex;
|
|
187
|
+
const desc = item.description ? ` ${item.description}` : "";
|
|
188
|
+
if (selected) {
|
|
189
|
+
all.push(`${INVERSE} ${item.name}${desc} ${RESET}`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
all.push(` ${item.name}${DIM}${desc}${RESET}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
let promptStartIdx = -1;
|
|
197
|
+
let cursorRowOffset = 0;
|
|
198
|
+
let cursorCol = 0;
|
|
170
199
|
if (ctx.phase === "input") {
|
|
171
|
-
const
|
|
172
|
-
|
|
200
|
+
const w = ctx.width;
|
|
201
|
+
const prefixLen = this.config.promptIcon.length + 1;
|
|
202
|
+
const styledPrefix = `\x1b[36m${this.config.promptIcon}${RESET} `;
|
|
203
|
+
const input = ctx.inputBuffer;
|
|
204
|
+
const firstLineCap = Math.max(1, w - prefixLen);
|
|
205
|
+
promptStartIdx = all.length;
|
|
206
|
+
if (input.length === 0) {
|
|
207
|
+
all.push(styledPrefix);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
all.push(styledPrefix + input.slice(0, firstLineCap));
|
|
211
|
+
for (let i = firstLineCap; i < input.length; i += w) {
|
|
212
|
+
all.push(input.slice(i, i + w));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const cursorVp = prefixLen + ctx.inputCursor;
|
|
216
|
+
cursorRowOffset = Math.floor(cursorVp / w);
|
|
217
|
+
cursorCol = cursorVp % w;
|
|
218
|
+
// Cursor on an exact wrap boundary lands past the last rendered row.
|
|
219
|
+
while (all.length - promptStartIdx <= cursorRowOffset) {
|
|
220
|
+
all.push("");
|
|
221
|
+
}
|
|
173
222
|
}
|
|
174
223
|
// Scroll: auto-scroll to bottom unless user manually scrolled
|
|
175
224
|
let offset = ctx.scrollOffset;
|
|
@@ -185,24 +234,20 @@ export class FloatingPanel {
|
|
|
185
234
|
}
|
|
186
235
|
this.scrollOffset = offset;
|
|
187
236
|
const visible = all.slice(offset, offset + ctx.height);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// If prompt is visible, set cursor
|
|
192
|
-
if (promptRow >= 0) {
|
|
237
|
+
if (ctx.phase === "input" && promptStartIdx >= 0) {
|
|
238
|
+
const cursorRowInVisible = promptStartIdx + cursorRowOffset - offset;
|
|
239
|
+
if (cursorRowInVisible >= 0 && cursorRowInVisible < visible.length) {
|
|
193
240
|
return {
|
|
194
241
|
lines: visible,
|
|
195
|
-
cursor: { row:
|
|
242
|
+
cursor: { row: cursorRowInVisible, col: cursorCol },
|
|
196
243
|
};
|
|
197
244
|
}
|
|
198
245
|
}
|
|
199
246
|
return { lines: visible };
|
|
200
247
|
});
|
|
201
|
-
// Default submit: no-op (extension overrides)
|
|
202
248
|
this.handlers.define(`${p}:submit`, (_query) => { });
|
|
203
|
-
|
|
204
|
-
this.handlers.define(`${p}:
|
|
205
|
-
// Default show: no-op (extension overrides to rebuild content on re-show)
|
|
249
|
+
this.handlers.define(`${p}:hide`, () => { });
|
|
250
|
+
this.handlers.define(`${p}:reset`, () => { });
|
|
206
251
|
this.handlers.define(`${p}:show`, () => { });
|
|
207
252
|
// Default custom input handler: don't consume
|
|
208
253
|
this.handlers.define(`${p}:input`, (_data) => false);
|
|
@@ -342,6 +387,7 @@ export class FloatingPanel {
|
|
|
342
387
|
this.ensureBuffer();
|
|
343
388
|
this.phase = "input";
|
|
344
389
|
this.editor.clear();
|
|
390
|
+
this.clearAutocomplete();
|
|
345
391
|
this.contentLines = [];
|
|
346
392
|
this.currentPartialLine = "";
|
|
347
393
|
this.scrollOffset = 0;
|
|
@@ -373,7 +419,7 @@ export class FloatingPanel {
|
|
|
373
419
|
// Agent idle or done — full teardown, hand back control.
|
|
374
420
|
this.teardownScreen();
|
|
375
421
|
}
|
|
376
|
-
this.handlers.call(`${this.prefix}:
|
|
422
|
+
this.handlers.call(`${this.prefix}:hide`);
|
|
377
423
|
}
|
|
378
424
|
/** Show the panel again after hide(), preserving conversation. */
|
|
379
425
|
show() {
|
|
@@ -394,14 +440,27 @@ export class FloatingPanel {
|
|
|
394
440
|
}
|
|
395
441
|
this.handlers.call(`${this.prefix}:show`);
|
|
396
442
|
}
|
|
397
|
-
/**
|
|
398
|
-
|
|
443
|
+
/** End the conversation: screen down and all buffered state cleared. */
|
|
444
|
+
reset() {
|
|
399
445
|
if (this.phase === "idle")
|
|
400
446
|
return;
|
|
401
447
|
if (this.autoDismissTimer) {
|
|
402
448
|
clearTimeout(this.autoDismissTimer);
|
|
403
449
|
this.autoDismissTimer = null;
|
|
404
450
|
}
|
|
451
|
+
this.teardownToHidden();
|
|
452
|
+
this.phase = "idle";
|
|
453
|
+
this.editor.clear();
|
|
454
|
+
this.clearAutocomplete();
|
|
455
|
+
this.contentLines = [];
|
|
456
|
+
this.currentPartialLine = "";
|
|
457
|
+
this.scrollOffset = 0;
|
|
458
|
+
this.title = "";
|
|
459
|
+
this.footer = "";
|
|
460
|
+
this.handlers.call(`${this.prefix}:reset`);
|
|
461
|
+
}
|
|
462
|
+
/** Screen-only teardown; conversation state is left untouched. */
|
|
463
|
+
teardownToHidden() {
|
|
405
464
|
if (this._passthrough) {
|
|
406
465
|
this.stopPassthrough();
|
|
407
466
|
this._passthrough = false;
|
|
@@ -416,13 +475,6 @@ export class FloatingPanel {
|
|
|
416
475
|
this.prevFrame = [];
|
|
417
476
|
this.teardownScreen();
|
|
418
477
|
}
|
|
419
|
-
this.phase = "idle";
|
|
420
|
-
this.editor.clear();
|
|
421
|
-
this.contentLines = [];
|
|
422
|
-
this.currentPartialLine = "";
|
|
423
|
-
this.scrollOffset = 0;
|
|
424
|
-
this.title = "";
|
|
425
|
-
this.footer = "";
|
|
426
478
|
}
|
|
427
479
|
/** Common screen enter logic shared by open() and show(). */
|
|
428
480
|
enterScreen() {
|
|
@@ -489,24 +541,25 @@ export class FloatingPanel {
|
|
|
489
541
|
this.phase = "active";
|
|
490
542
|
}
|
|
491
543
|
setDone() {
|
|
492
|
-
if (this._passthrough) {
|
|
493
|
-
// Agent finished while hidden — session over, hand back control.
|
|
494
|
-
this.dismiss();
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
544
|
if (this.config.autoDismissMs > 0) {
|
|
498
|
-
// Legacy behavior: enter done state, auto-dismiss after delay
|
|
499
545
|
this.phase = "done";
|
|
500
|
-
this.render();
|
|
501
546
|
this.autoDismissTimer = setTimeout(() => {
|
|
502
547
|
if (this.phase === "done")
|
|
503
|
-
this.
|
|
548
|
+
this.reset();
|
|
504
549
|
}, this.config.autoDismissMs);
|
|
505
550
|
}
|
|
506
551
|
else {
|
|
507
|
-
// Auto-prompt: transition to input for follow-up conversation
|
|
508
552
|
this.phase = "input";
|
|
509
553
|
this.editor.clear();
|
|
554
|
+
this.clearAutocomplete();
|
|
555
|
+
}
|
|
556
|
+
if (this._passthrough) {
|
|
557
|
+
// Agent finished while hidden — release the screen but keep state
|
|
558
|
+
// so the next summon resumes the transcript.
|
|
559
|
+
this.teardownToHidden();
|
|
560
|
+
this.handlers.call(`${this.prefix}:hide`);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
510
563
|
this.render();
|
|
511
564
|
}
|
|
512
565
|
}
|
|
@@ -526,6 +579,64 @@ export class FloatingPanel {
|
|
|
526
579
|
requestRender() {
|
|
527
580
|
this.scheduleRender();
|
|
528
581
|
}
|
|
582
|
+
// ── Autocomplete helpers ────────────────────────────────────
|
|
583
|
+
updateAutocomplete() {
|
|
584
|
+
if (this.phase !== "input") {
|
|
585
|
+
this.clearAutocomplete();
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const buf = this.editor.text;
|
|
589
|
+
let command = null;
|
|
590
|
+
let commandArgs = null;
|
|
591
|
+
if (buf.startsWith("/")) {
|
|
592
|
+
const spaceIdx = buf.indexOf(" ");
|
|
593
|
+
if (spaceIdx !== -1) {
|
|
594
|
+
command = buf.slice(0, spaceIdx);
|
|
595
|
+
commandArgs = buf.slice(spaceIdx + 1);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const { items } = this.bus.emitPipe("autocomplete:request", {
|
|
599
|
+
buffer: buf,
|
|
600
|
+
command,
|
|
601
|
+
commandArgs,
|
|
602
|
+
items: [],
|
|
603
|
+
});
|
|
604
|
+
if (items.length > 0) {
|
|
605
|
+
this.autocompleteItems = items;
|
|
606
|
+
this.autocompleteActive = true;
|
|
607
|
+
if (this.autocompleteIndex >= items.length)
|
|
608
|
+
this.autocompleteIndex = 0;
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
this.clearAutocomplete();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
applyAutocomplete() {
|
|
615
|
+
if (!this.autocompleteActive || this.autocompleteItems.length === 0)
|
|
616
|
+
return false;
|
|
617
|
+
const sel = this.autocompleteItems[this.autocompleteIndex];
|
|
618
|
+
if (!sel)
|
|
619
|
+
return false;
|
|
620
|
+
// For @file completion only the partial after the last @ is replaced.
|
|
621
|
+
const text = this.editor.text;
|
|
622
|
+
const atPos = text.lastIndexOf("@");
|
|
623
|
+
const isFileAc = atPos >= 0
|
|
624
|
+
&& (atPos === 0 || text[atPos - 1] === " ")
|
|
625
|
+
&& !text.slice(atPos + 1).includes(" ");
|
|
626
|
+
if (isFileAc) {
|
|
627
|
+
this.editor.setText(text.slice(0, atPos) + "@" + sel.name);
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
this.editor.setText(sel.name);
|
|
631
|
+
}
|
|
632
|
+
this.clearAutocomplete();
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
clearAutocomplete() {
|
|
636
|
+
this.autocompleteActive = false;
|
|
637
|
+
this.autocompleteItems = [];
|
|
638
|
+
this.autocompleteIndex = 0;
|
|
639
|
+
}
|
|
529
640
|
// ── Input handling ──────────────────────────────────────────
|
|
530
641
|
handleIntercept(payload) {
|
|
531
642
|
const consumed = { ...payload, consumed: true };
|
|
@@ -541,7 +652,7 @@ export class FloatingPanel {
|
|
|
541
652
|
}
|
|
542
653
|
switch (this.phase) {
|
|
543
654
|
case "done":
|
|
544
|
-
this.
|
|
655
|
+
this.reset();
|
|
545
656
|
return consumed;
|
|
546
657
|
case "input":
|
|
547
658
|
this.handleInputKey(data);
|
|
@@ -568,41 +679,40 @@ export class FloatingPanel {
|
|
|
568
679
|
return payload;
|
|
569
680
|
}
|
|
570
681
|
}
|
|
571
|
-
/**
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
682
|
+
/**
|
|
683
|
+
* Handle scroll input. Returns true if consumed.
|
|
684
|
+
* Pass `includeArrows=false` in input phase so arrows reach the editor.
|
|
685
|
+
*/
|
|
686
|
+
handleScroll(data, includeArrows = true) {
|
|
687
|
+
if (includeArrows) {
|
|
688
|
+
if (data === "\x1b[A" || data === "\x1bOA") {
|
|
689
|
+
this.scrollUp(1);
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
if (data === "\x1b[B" || data === "\x1bOB") {
|
|
693
|
+
this.scrollDown(1);
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
582
696
|
}
|
|
583
|
-
// Page up (CSI 5~)
|
|
584
697
|
if (data === "\x1b[5~") {
|
|
585
698
|
this.scrollUp(this.computeGeometry().contentH - 1);
|
|
586
699
|
return true;
|
|
587
700
|
}
|
|
588
|
-
// Page down (CSI 6~)
|
|
589
701
|
if (data === "\x1b[6~") {
|
|
590
702
|
this.scrollDown(this.computeGeometry().contentH - 1);
|
|
591
703
|
return true;
|
|
592
704
|
}
|
|
593
|
-
// Mouse wheel: CSI M followed by button byte (64 = wheel up, 65 = wheel down)
|
|
594
705
|
if (data.length >= 6 && data.startsWith("\x1b[M")) {
|
|
595
706
|
const button = data.charCodeAt(3);
|
|
596
707
|
if (button === 96) {
|
|
597
708
|
this.scrollUp(3);
|
|
598
709
|
return true;
|
|
599
|
-
}
|
|
710
|
+
}
|
|
600
711
|
if (button === 97) {
|
|
601
712
|
this.scrollDown(3);
|
|
602
713
|
return true;
|
|
603
|
-
}
|
|
714
|
+
}
|
|
604
715
|
}
|
|
605
|
-
// SGR mouse: CSI < 64;x;yM (wheel up) / CSI < 65;x;yM (wheel down)
|
|
606
716
|
const sgr = data.match(/^\x1b\[<(64|65);\d+;\d+M$/);
|
|
607
717
|
if (sgr) {
|
|
608
718
|
if (sgr[1] === "64") {
|
|
@@ -617,59 +727,92 @@ export class FloatingPanel {
|
|
|
617
727
|
return false;
|
|
618
728
|
}
|
|
619
729
|
handleInputKey(data) {
|
|
620
|
-
// Check full data string against trigger sequences (may be multi-byte)
|
|
621
730
|
if (this.isTrigger(data)) {
|
|
622
731
|
this.hide();
|
|
623
732
|
return;
|
|
624
733
|
}
|
|
625
734
|
for (let i = 0; i < data.length; i++) {
|
|
626
735
|
const ch = data[i];
|
|
627
|
-
if (ch === "\x1b" && data[i + 1] == null) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
736
|
+
if ((ch === "\x1b" && data[i + 1] == null) || ch.charCodeAt(0) === 0x03) {
|
|
737
|
+
// First Esc/Ctrl+C closes the dropdown; second hides the panel.
|
|
738
|
+
if (this.autocompleteActive) {
|
|
739
|
+
this.clearAutocomplete();
|
|
740
|
+
this.render();
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
632
743
|
this.hide();
|
|
633
744
|
return;
|
|
634
745
|
}
|
|
635
746
|
}
|
|
636
|
-
|
|
637
|
-
if (this.handleScroll(data))
|
|
747
|
+
if (this.handleScroll(data, false))
|
|
638
748
|
return;
|
|
639
749
|
const actions = this.editor.feed(data);
|
|
640
750
|
for (const action of actions) {
|
|
641
751
|
switch (action.action) {
|
|
642
752
|
case "submit": {
|
|
753
|
+
// Apply selection on Enter so it both picks and submits.
|
|
754
|
+
this.applyAutocomplete();
|
|
643
755
|
const query = this.editor.text.trim();
|
|
644
756
|
if (!query) {
|
|
645
757
|
this.hide();
|
|
646
758
|
return;
|
|
647
759
|
}
|
|
648
760
|
this.editor.pushHistory(query);
|
|
649
|
-
this.phase = "active";
|
|
650
761
|
this.editor.clear();
|
|
762
|
+
this.clearAutocomplete();
|
|
763
|
+
// Phase change is the submit handler's call — sync slash commands
|
|
764
|
+
// (e.g. /model, /help) keep the user in input mode.
|
|
651
765
|
this.handlers.call(`${this.prefix}:submit`, query);
|
|
652
766
|
return;
|
|
653
767
|
}
|
|
654
768
|
case "cancel":
|
|
769
|
+
if (this.autocompleteActive) {
|
|
770
|
+
this.clearAutocomplete();
|
|
771
|
+
this.render();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
655
774
|
this.hide();
|
|
656
775
|
return;
|
|
776
|
+
case "tab":
|
|
777
|
+
// Re-query after applying a command name so arg completions show.
|
|
778
|
+
if (this.applyAutocomplete())
|
|
779
|
+
this.updateAutocomplete();
|
|
780
|
+
this.render();
|
|
781
|
+
break;
|
|
782
|
+
case "shift+tab":
|
|
783
|
+
this.render();
|
|
784
|
+
break;
|
|
657
785
|
case "arrow-up": {
|
|
658
|
-
|
|
659
|
-
|
|
786
|
+
if (this.autocompleteActive) {
|
|
787
|
+
this.autocompleteIndex = this.autocompleteIndex === 0
|
|
788
|
+
? this.autocompleteItems.length - 1
|
|
789
|
+
: this.autocompleteIndex - 1;
|
|
660
790
|
this.render();
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
const hist = this.editor.historyBack();
|
|
794
|
+
if (hist)
|
|
795
|
+
this.render();
|
|
796
|
+
}
|
|
661
797
|
break;
|
|
662
798
|
}
|
|
663
799
|
case "arrow-down": {
|
|
664
|
-
|
|
665
|
-
|
|
800
|
+
if (this.autocompleteActive) {
|
|
801
|
+
this.autocompleteIndex = this.autocompleteIndex === this.autocompleteItems.length - 1
|
|
802
|
+
? 0
|
|
803
|
+
: this.autocompleteIndex + 1;
|
|
666
804
|
this.render();
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
const hist = this.editor.historyForward();
|
|
808
|
+
if (hist)
|
|
809
|
+
this.render();
|
|
810
|
+
}
|
|
667
811
|
break;
|
|
668
812
|
}
|
|
669
813
|
case "changed":
|
|
670
|
-
case "tab":
|
|
671
|
-
case "shift+tab":
|
|
672
814
|
case "delete-empty":
|
|
815
|
+
this.updateAutocomplete();
|
|
673
816
|
this.render();
|
|
674
817
|
break;
|
|
675
818
|
}
|