@oddessentials/agent-guild 0.23.0 → 0.24.1

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/web/index.html CHANGED
@@ -125,6 +125,7 @@
125
125
  </div>
126
126
  <div class="panel-actions">
127
127
  <button id="panel-voice" class="btn voice" type="button" hidden aria-pressed="false" title="Speak into this terminal. Review the text, then press Enter yourself.">Dictate</button>
128
+ <button id="panel-copy" class="btn" type="button" hidden aria-haspopup="dialog" title="Select and copy terminal text">Copy…</button>
128
129
  <button id="panel-stop" class="btn danger" type="button">Stop</button>
129
130
  <button id="panel-close" class="btn" type="button" title="Hide the terminal. The session keeps running.">Hide</button>
130
131
  </div>
@@ -133,6 +134,21 @@
133
134
  <div id="voice-preview" class="voice-preview" hidden aria-live="polite"></div>
134
135
  </div>
135
136
 
137
+ <dialog id="terminal-copy" class="terminal-copy" aria-labelledby="terminal-copy-title">
138
+ <div class="terminal-copy-bar">
139
+ <div class="terminal-copy-heading">
140
+ <h2 id="terminal-copy-title">Copy terminal text</h2>
141
+ <p class="sub" data-session></p>
142
+ </div>
143
+ <button class="btn" type="button" data-done>Done</button>
144
+ </div>
145
+ <textarea readonly autofocus wrap="off" spellcheck="false" inputmode="none" aria-label="Terminal text snapshot" aria-describedby="terminal-copy-status"></textarea>
146
+ <div class="terminal-copy-footer">
147
+ <p id="terminal-copy-status" role="status"></p>
148
+ <button class="btn primary" type="button" data-copy disabled>Copy selection</button>
149
+ </div>
150
+ </dialog>
151
+
136
152
  <dialog id="models" class="models" aria-labelledby="models-title">
137
153
  <div class="models-bar">
138
154
  <span id="models-icon" class="provider-icon small"></span>
@@ -304,6 +320,11 @@
304
320
  <summary></summary>
305
321
  <ul></ul>
306
322
  </details>
323
+ <details class="multiplexers" hidden>
324
+ <summary>Terminal multiplexers</summary>
325
+ <button class="btn multiplexer-refresh" type="button">Refresh</button>
326
+ <div class="multiplexer-list"></div>
327
+ </details>
307
328
  <div class="provider-actions">
308
329
  <button class="btn primary new" type="button">New</button>
309
330
  <button class="btn existing" type="button">Existing…</button>
package/web/styles.css CHANGED
@@ -418,6 +418,27 @@ body:has(#app[hidden]):has(#auth[hidden]):has(#stopped[hidden])::after {
418
418
  }
419
419
  .terminal-host { flex: 1; min-height: 0; padding: 8px 4px 4px 10px; }
420
420
  .terminal-host > div { height: 100%; }
421
+ .terminal-copy {
422
+ width: min(880px, calc(100vw - 24px)); height: min(720px, calc(100dvh - 24px));
423
+ max-width: none; max-height: none; padding: 0; overflow: hidden;
424
+ color: var(--text); background: var(--surface); border: 1px solid var(--border);
425
+ border-radius: var(--radius); box-shadow: var(--shadow);
426
+ }
427
+ .terminal-copy[open] { display: flex; flex-direction: column; }
428
+ .terminal-copy::backdrop { background: rgba(15, 17, 21, 0.5); }
429
+ .terminal-copy-bar, .terminal-copy-footer { display: flex; align-items: center; gap: 12px; padding: 12px 16px; }
430
+ .terminal-copy-heading { flex: 1; min-width: 0; }
431
+ .terminal-copy-heading .sub { margin: 4px 0 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-size: 0.8rem; }
432
+ .terminal-copy textarea {
433
+ flex: 1; min-height: 0; width: 100%; resize: none; margin: 0; padding: 14px 16px;
434
+ border: 0; border-block: 1px solid var(--border); border-radius: 0;
435
+ background: var(--term-bg); color: #e6e9ef; font: 14px/1.5 var(--font-mono);
436
+ white-space: pre; user-select: text; -webkit-user-select: text; touch-action: auto; overscroll-behavior: contain;
437
+ }
438
+ .terminal-copy textarea:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
439
+ .terminal-copy-footer p { flex: 1; margin: 0; color: var(--muted); font-size: 0.8rem; }
440
+ .terminal-copy .btn { min-height: 44px; flex-shrink: 0; }
441
+ .terminal-copy .btn:disabled { cursor: default; }
421
442
  @media (pointer: coarse) {
422
443
  /* A drag on the panel would otherwise scroll the page behind it. */
423
444
  body:has(#terminal-panel:not([hidden])) { overflow: hidden; }
@@ -736,3 +757,13 @@ a.news-link:visited { color: var(--muted); }
736
757
  .history-row .session-id { grid-row: 2; justify-self: start; }
737
758
  .history-form { flex-wrap: wrap; }
738
759
  }
760
+ /* Installation controls share each skin's card colors and button styles. */
761
+ .multiplexers { margin: .6rem 0; }
762
+ .multiplexers > summary { cursor: pointer; font-size: .85rem; }
763
+ .multiplexer-refresh { margin: .5rem 0; }
764
+ .multiplexer-row { padding: .65rem 0; border-top: 1px solid var(--border); }
765
+ .multiplexer-row > strong { margin-right: .6rem; }
766
+ .multiplexer-note { font-size: .8rem; line-height: 1.5; overflow-wrap: anywhere; margin: .4rem 0; }
767
+ .multiplexer-row .copies { margin-top: .4rem; }
768
+ .multiplexer-row .copy-head { flex-wrap: wrap; }
769
+ .multiplexer-row .copy-name { flex: 1 0 100%; }
@@ -0,0 +1,128 @@
1
+ // A frozen copy surface keeps native touch selection separate from terminal gestures.
2
+ const SELECT_HINT = 'Touch and hold to select text.';
3
+
4
+ export function captureTerminalText(term) {
5
+ const buffer = term.buffer.active;
6
+ const lines = [];
7
+ let viewportLine = 0;
8
+ let viewportPrefix = '';
9
+ for (let y = 0; y < buffer.length; y++) {
10
+ const line = buffer.getLine(y);
11
+ const text = line.translateToString(true, 0, term.cols);
12
+ const wrapped = line.isWrapped && lines.length > 0;
13
+ if (y === buffer.viewportY) {
14
+ viewportLine = wrapped ? lines.length - 1 : lines.length;
15
+ // The visible row can begin inside a joined line. Keep its actual text
16
+ // prefix: terminal cells and JavaScript character counts differ for Unicode.
17
+ viewportPrefix = wrapped ? lines[lines.length - 1] : '';
18
+ }
19
+ if (wrapped) lines[lines.length - 1] += text;
20
+ else lines.push(text);
21
+ }
22
+ return { text: lines.join('\n'), viewportLine, viewportPrefix };
23
+ }
24
+
25
+ export class TerminalCopy {
26
+ constructor({ opener, dialog, getCurrent }) {
27
+ this.dialog = dialog;
28
+ this.getCurrent = getCurrent;
29
+ this.text = dialog.querySelector('textarea');
30
+ this.copyButton = dialog.querySelector('[data-copy]');
31
+ this.status = dialog.querySelector('[role="status"]');
32
+ this.current = null;
33
+ this.attempt = null;
34
+ const touch = matchMedia('(any-pointer: coarse)');
35
+ const availability = () => { opener.hidden = !touch.matches && !navigator.maxTouchPoints; };
36
+ touch.addEventListener('change', availability);
37
+ availability();
38
+ opener.addEventListener('click', () => this.open());
39
+ this.copyButton.addEventListener('click', () => this.copy());
40
+ dialog.querySelector('[data-done]').addEventListener('click', () => this.close(true));
41
+ dialog.addEventListener('cancel', (event) => { event.preventDefault(); this.close(true); });
42
+ // A programmatic/native close can arrive after the sheet has already reopened.
43
+ dialog.addEventListener('close', () => { if (!dialog.open) this.clear(); });
44
+ this.text.addEventListener('select', () => this.selectionChanged());
45
+ document.addEventListener('selectionchange', () => {
46
+ if (dialog.open) this.selectionChanged();
47
+ });
48
+ }
49
+
50
+ open() {
51
+ if (this.dialog.open) return;
52
+ const current = this.getCurrent();
53
+ if (!current) return;
54
+ const snapshot = captureTerminalText(current.term);
55
+ this.current = current;
56
+ this.selection = '0:0';
57
+ this.text.value = snapshot.text;
58
+ this.dialog.querySelector('[data-session]').textContent = current.name;
59
+ this.status.textContent = SELECT_HINT;
60
+ this.copyButton.disabled = true;
61
+ this.dialog.showModal();
62
+ this.text.focus({ preventScroll: true });
63
+ this.text.setSelectionRange(0, 0);
64
+ const style = getComputedStyle(this.text);
65
+ this.text.scrollTop = snapshot.viewportLine * parseFloat(style.lineHeight);
66
+ this.text.scrollLeft = 0;
67
+ if (snapshot.viewportPrefix) {
68
+ const measure = document.createElement('canvas').getContext('2d');
69
+ measure.font = style.font;
70
+ this.text.scrollLeft = measure.measureText(snapshot.viewportPrefix).width;
71
+ }
72
+ }
73
+
74
+ selectionChanged() {
75
+ const selection = `${this.text.selectionStart}:${this.text.selectionEnd}`;
76
+ if (selection !== this.selection && this.status.textContent === 'Copied') {
77
+ this.status.textContent = SELECT_HINT;
78
+ }
79
+ this.selection = selection;
80
+ this.copyButton.disabled = Boolean(this.attempt) || this.text.selectionStart === this.text.selectionEnd;
81
+ }
82
+
83
+ async copy() {
84
+ const current = this.current;
85
+ if (!current || this.attempt) return;
86
+ const range = `${this.text.selectionStart}:${this.text.selectionEnd}`;
87
+ const selected = this.text.value.slice(this.text.selectionStart, this.text.selectionEnd);
88
+ if (!selected) return;
89
+ const attempt = this.attempt = {};
90
+ this.copyButton.disabled = true;
91
+ this.status.textContent = 'Copying…';
92
+ try {
93
+ // Stay in the user's click activation; no permission preflight or clipboard read.
94
+ // Textareas normalize newlines to LF; match xterm's Windows copy convention.
95
+ const text = /Win/.test(navigator.platform) ? selected.replace(/\n/g, '\r\n') : selected;
96
+ await navigator.clipboard.writeText(text);
97
+ if (this.current === current && this.attempt === attempt) {
98
+ this.status.textContent = range === `${this.text.selectionStart}:${this.text.selectionEnd}` ? 'Copied' : SELECT_HINT;
99
+ }
100
+ } catch {
101
+ if (this.current === current && this.attempt === attempt) {
102
+ this.status.textContent = 'Use Copy in the text selection menu.';
103
+ this.text.focus({ preventScroll: true });
104
+ }
105
+ } finally {
106
+ if (this.attempt === attempt) {
107
+ this.attempt = null;
108
+ this.selectionChanged();
109
+ }
110
+ }
111
+ }
112
+
113
+ close(restoreFocus = false) {
114
+ const current = this.current;
115
+ this.clear();
116
+ if (this.dialog.open) this.dialog.close();
117
+ if (restoreFocus && current && this.getCurrent()?.term === current.term) current.term.focus();
118
+ }
119
+
120
+ clear() {
121
+ this.current = null;
122
+ this.attempt = null;
123
+ this.text.value = '';
124
+ this.status.textContent = '';
125
+ this.dialog.querySelector('[data-session]').textContent = '';
126
+ this.copyButton.disabled = true;
127
+ }
128
+ }