Spaces:
Running
Running
Agent Manager commited on
Commit ·
7ef25ac
1
Parent(s): a5d1eab
Name the modes for their form: terminal ⇄ reader
Browse files"Conversation" described the terminal just as well — both modes show the same
session; what differs is the form. Reader mode borrows the browser's idea: the
same content, laid out to be read. The toggle reads `terminal | reader`, the
type is `PaneMode = 'terminal' | 'reader'`, and the overlay is `.pane-reader`.
app-shots.mjs now resets the fleet before it runs — half its checks compare
pane counts, and previous runs left groups and spare shells behind.
- docs/conversation-view.md +18 -16
- web/src/App.tsx +8 -8
- web/src/components/Overview.tsx +2 -2
- web/src/components/TerminalPane.tsx +7 -7
- web/src/conversation.css +3 -3
- web/src/lib/paneMode.ts +3 -3
docs/conversation-view.md
CHANGED
|
@@ -60,7 +60,7 @@ Four depths of one component:
|
|
| 60 |
| **D0** brief | Overview tile | prompt (1 line) + state |
|
| 61 |
| **D1** card | Overview card, collapsed | prompt, one meta line, answer, reply box |
|
| 62 |
| **D2** open | Overview card, unfolded | + the steps between prompt and answer; `↑ show previous turn` walks back one exchange at a time |
|
| 63 |
-
| **D3** full | Session pane,
|
| 64 |
|
| 65 |
The viewer stops being a different thing and becomes **a vertical stack of the card**. That is
|
| 66 |
the whole design; the rest is consequences.
|
|
@@ -150,7 +150,7 @@ Rules that keep this from becoming the ugly viewer in a small box:
|
|
| 150 |
- **Only the card you opened reads the trace.** Inline in the Overview list a card is a
|
| 151 |
summary — one prompt, one clamped answer — and the digest already has that. Reading the trace
|
| 152 |
per visible agent, and polling it every three seconds for the working ones, would turn the
|
| 153 |
-
Overview from one `/api/meta` poll into one transcript read per row. The window (and
|
| 154 |
is where the middle gets fetched.
|
| 155 |
- **A card says nothing the surface around it already says.** No turn number (there is one turn),
|
| 156 |
no clock (its header carries `·6m`), no model, no harness. In the viewer the same line's right
|
|
@@ -163,7 +163,7 @@ exchange per click**, never by "load everything":
|
|
| 163 |
- A centred `↑ show previous turn` at the top of the body prepends the previous exchange,
|
| 164 |
collapsed to prompt + answer (its steps fold behind the same `▸ n steps`). Centred, because it
|
| 165 |
belongs to the whole card rather than to the column of text under it.
|
| 166 |
-
- After the second one, `full history ↗` joins it, opening the pane in
|
| 167 |
exchange. The card is for "what just happened"; archaeology has a bigger room, and the handoff
|
| 168 |
is one click.
|
| 169 |
- Cap the card at, say, 5 exchanges regardless — past that the card is the wrong tool and says so.
|
|
@@ -183,12 +183,12 @@ was a workaround for not having the middle; with the middle visible it has nothi
|
|
| 183 |
`--vvh` is already maintained from `window.visualViewport` (`App.tsx:106-114`); the backdrop
|
| 184 |
must use it, or an open keyboard covers the reply line it is there to serve.
|
| 185 |
|
| 186 |
-
### 3.3 The session pane: terminal ⇄
|
| 187 |
|
| 188 |
The bottom bar gets a two-state control, next to the zoom:
|
| 189 |
|
| 190 |
```
|
| 191 |
-
[ terminal |
|
| 192 |
```
|
| 193 |
|
| 194 |
**It is one setting for the whole app, like zoom** — not a per-pane toggle. Reading a fleet means
|
|
@@ -199,13 +199,15 @@ pane with nothing to render (a shell) simply stays a terminal.
|
|
| 199 |
the files/trace panels are not sessions at all — same rule the Overview uses to decide what is
|
| 200 |
an agent (`cli !== 'shell' && !isPassive(cli)`).
|
| 201 |
- **terminal** — today's terminal, untouched.
|
| 202 |
-
- **
|
|
|
|
|
|
|
| 203 |
- The mode is a **view preference**, kept in `localStorage` for the app, not in the store.
|
| 204 |
-
- The terminal element stays mounted and connected underneath; the
|
| 205 |
must not detach tmux — a reattach costs a repaint and can trip the handoff path
|
| 206 |
(`HANDOFF_CODE`, `TerminalPane.tsx`). **Verify** this before shipping: xterm needs layout to
|
| 207 |
fit, so "cover, don't unmount" is the low-risk option, and a refit on return is required.
|
| 208 |
-
- The
|
| 209 |
first row has no spare width. It carries only what is true of the whole session and said
|
| 210 |
nowhere else: the model, `13 turns`, the token totals abbreviated (`2.2M↓ 654k↑`), `▲▼`, and
|
| 211 |
the search box. The harness is the logo in the row above; the raw message count and the cached
|
|
@@ -219,7 +221,7 @@ pane with nothing to render (a shell) simply stays a terminal.
|
|
| 219 |
the nearest positioned ancestor and lands a few rows off.
|
| 220 |
- Vocabulary: a **turn** is one exchange, a **message** is a raw transcript row. Each turn's meta
|
| 221 |
line says `turn 6/13`, so the bar counts the same things the reader does.
|
| 222 |
-
- **The terminal must not keep the keyboard** while
|
| 223 |
swallows keystrokes into the agent's TTY, invisibly — and several paths grab focus back (the
|
| 224 |
pane becoming active, the header, the key bar), some of them *after* the mode changes, so the
|
| 225 |
guard belongs at each call rather than at the switch.
|
|
@@ -233,10 +235,10 @@ pane with nothing to render (a shell) simply stays a terminal.
|
|
| 233 |
tool call is unusable while a task runs; one that never moves makes you chase it.
|
| 234 |
- **The prompt band sticks to the top** while you read a long turn. What you want overhead deep
|
| 235 |
in someone's 67-step answer is the question it is answering — not a row of numbers.
|
| 236 |
-
- **The
|
| 237 |
side while the prompt band still spanned the full width, so the two disagreed about where the
|
| 238 |
conversation began. The pane is the measure: narrow the pane and the conversation narrows.
|
| 239 |
-
- **
|
| 240 |
card has always known that, and a rendered session that could only be read would send you back
|
| 241 |
to the terminal to type. It is the card's own composer (`.ov-live`), the same `sendInput`, and the
|
| 242 |
same optimistic echo: your prompt appears at the bottom with a `working` line until the
|
|
@@ -250,11 +252,11 @@ pane with nothing to render (a shell) simply stays a terminal.
|
|
| 250 |
|
| 251 |
| Today | Tomorrow |
|
| 252 |
|---|---|
|
| 253 |
-
| `Read this session's trace` on every agent row (`Sidebar.tsx:245`) | bottom bar → **
|
| 254 |
| `Share this session` on every agent row (`Sidebar.tsx:246`) | pane header → **share** |
|
| 255 |
| A `trace` **session row per read** (`App.tsx:openTrace`) | **gone** — no duplicate rows |
|
| 256 |
| Trace row's `Share` (`Sidebar.tsx:237`) | trace pane header (imported traces keep a pane) |
|
| 257 |
-
| Trace row's `Handover` (`Sidebar.tsx:238`) |
|
| 258 |
| Quick-add **Trace** = open a shared dataset (`Sidebar.tsx:482`) | **stays** |
|
| 259 |
|
| 260 |
The last row is deliberate: an **imported** trace has no session behind it, so it is a genuine
|
|
@@ -376,7 +378,7 @@ the block model already supports.
|
|
| 376 |
|
| 377 |
1. **How far back in the card?** Proposal: one exchange per click, hard cap 5, then hand off to
|
| 378 |
the conversation. Alternative: one, then straight to the full conversation.
|
| 379 |
-
2. **Does the app open in
|
| 380 |
3. **Kill the `↑ ↓` turn stepper?** Proposal: yes — unfolding replaces it.
|
| 381 |
4. **Unfolded card of a running agent**: live-refresh every 3 s, or freeze until it finishes
|
| 382 |
(cheaper, less alive)?
|
|
@@ -421,7 +423,7 @@ Built (this branch):
|
|
| 421 |
there is no transcript to read.
|
| 422 |
3. `pageOf()` takes a negative offset — "the last N turns" without a round trip to learn `total`
|
| 423 |
(`server/test/trace-tail.test.mjs`).
|
| 424 |
-
4. The session pane gets terminal ⇄
|
| 425 |
connected but loses the keyboard; the mode is a per-session view preference in `localStorage`
|
| 426 |
(`web/src/lib/paneMode.ts`), and its event reaches a pane that is already open — which is what
|
| 427 |
the card's "full history ↗" needs.
|
|
@@ -433,7 +435,7 @@ Not yet, in the order I would do it:
|
|
| 433 |
6. `head.prompts[]` (§5) — index, first line and timestamp per prompt, so a surface can draw the
|
| 434 |
skeleton and label "show previous turn" before fetching the page that holds it.
|
| 435 |
7. The sidebar loses its trace buttons and `openTrace`; share moves into the pane header (§3.4).
|
| 436 |
-
8. Windowing by exchange in
|
| 437 |
`head.prompts[]` gives the skeleton up front, and only an opened turn needs its page. The
|
| 438 |
measured-height machinery in `TraceView` is reused as-is — what changes is what a "row" means.
|
| 439 |
Until then it reads the last 400 turns in one request.
|
|
|
|
| 60 |
| **D0** brief | Overview tile | prompt (1 line) + state |
|
| 61 |
| **D1** card | Overview card, collapsed | prompt, one meta line, answer, reply box |
|
| 62 |
| **D2** open | Overview card, unfolded | + the steps between prompt and answer; `↑ show previous turn` walks back one exchange at a time |
|
| 63 |
+
| **D3** full | Session pane, reader mode | every exchange, windowed, each expandable to D2 |
|
| 64 |
|
| 65 |
The viewer stops being a different thing and becomes **a vertical stack of the card**. That is
|
| 66 |
the whole design; the rest is consequences.
|
|
|
|
| 150 |
- **Only the card you opened reads the trace.** Inline in the Overview list a card is a
|
| 151 |
summary — one prompt, one clamped answer — and the digest already has that. Reading the trace
|
| 152 |
per visible agent, and polling it every three seconds for the working ones, would turn the
|
| 153 |
+
Overview from one `/api/meta` poll into one transcript read per row. The window (and reader mode)
|
| 154 |
is where the middle gets fetched.
|
| 155 |
- **A card says nothing the surface around it already says.** No turn number (there is one turn),
|
| 156 |
no clock (its header carries `·6m`), no model, no harness. In the viewer the same line's right
|
|
|
|
| 163 |
- A centred `↑ show previous turn` at the top of the body prepends the previous exchange,
|
| 164 |
collapsed to prompt + answer (its steps fold behind the same `▸ n steps`). Centred, because it
|
| 165 |
belongs to the whole card rather than to the column of text under it.
|
| 166 |
+
- After the second one, `full history ↗` joins it, opening the pane in reader mode at that
|
| 167 |
exchange. The card is for "what just happened"; archaeology has a bigger room, and the handoff
|
| 168 |
is one click.
|
| 169 |
- Cap the card at, say, 5 exchanges regardless — past that the card is the wrong tool and says so.
|
|
|
|
| 183 |
`--vvh` is already maintained from `window.visualViewport` (`App.tsx:106-114`); the backdrop
|
| 184 |
must use it, or an open keyboard covers the reply line it is there to serve.
|
| 185 |
|
| 186 |
+
### 3.3 The session pane: terminal ⇄ reader
|
| 187 |
|
| 188 |
The bottom bar gets a two-state control, next to the zoom:
|
| 189 |
|
| 190 |
```
|
| 191 |
+
[ terminal | reader ] − 100% +
|
| 192 |
```
|
| 193 |
|
| 194 |
**It is one setting for the whole app, like zoom** — not a per-pane toggle. Reading a fleet means
|
|
|
|
| 199 |
the files/trace panels are not sessions at all — same rule the Overview uses to decide what is
|
| 200 |
an agent (`cli !== 'shell' && !isPassive(cli)`).
|
| 201 |
- **terminal** — today's terminal, untouched.
|
| 202 |
+
- **reader** — the same session, laid out: the exchange renderer over `/api/trace/:id`, at D3.
|
| 203 |
+
The two modes show the *same content*; what differs is the form, which is why the labels name
|
| 204 |
+
the form. "Conversation" would have described the terminal just as well.
|
| 205 |
- The mode is a **view preference**, kept in `localStorage` for the app, not in the store.
|
| 206 |
+
- The terminal element stays mounted and connected underneath; the reader draws over it. Toggling
|
| 207 |
must not detach tmux — a reattach costs a repaint and can trip the handoff path
|
| 208 |
(`HANDOFF_CODE`, `TerminalPane.tsx`). **Verify** this before shipping: xterm needs layout to
|
| 209 |
fit, so "cover, don't unmount" is the low-risk option, and a refit on return is required.
|
| 210 |
+
- The reader's toolbar is a second header row, not a squeeze into the first — on a phone the
|
| 211 |
first row has no spare width. It carries only what is true of the whole session and said
|
| 212 |
nowhere else: the model, `13 turns`, the token totals abbreviated (`2.2M↓ 654k↑`), `▲▼`, and
|
| 213 |
the search box. The harness is the logo in the row above; the raw message count and the cached
|
|
|
|
| 221 |
the nearest positioned ancestor and lands a few rows off.
|
| 222 |
- Vocabulary: a **turn** is one exchange, a **message** is a raw transcript row. Each turn's meta
|
| 223 |
line says `turn 6/13`, so the bar counts the same things the reader does.
|
| 224 |
+
- **The terminal must not keep the keyboard** while reader mode covers it. A mounted xterm with focus
|
| 225 |
swallows keystrokes into the agent's TTY, invisibly — and several paths grab focus back (the
|
| 226 |
pane becoming active, the header, the key bar), some of them *after* the mode changes, so the
|
| 227 |
guard belongs at each call rather than at the switch.
|
|
|
|
| 235 |
tool call is unusable while a task runs; one that never moves makes you chase it.
|
| 236 |
- **The prompt band sticks to the top** while you read a long turn. What you want overhead deep
|
| 237 |
in someone's 67-step answer is the question it is answering — not a row of numbers.
|
| 238 |
+
- **The reader fills the pane.** A fixed reading column left a gutter of nothing on each
|
| 239 |
side while the prompt band still spanned the full width, so the two disagreed about where the
|
| 240 |
conversation began. The pane is the measure: narrow the pane and the conversation narrows.
|
| 241 |
+
- **Reader mode can be replied to.** Reading a conversation and answering it are the same act — the
|
| 242 |
card has always known that, and a rendered session that could only be read would send you back
|
| 243 |
to the terminal to type. It is the card's own composer (`.ov-live`), the same `sendInput`, and the
|
| 244 |
same optimistic echo: your prompt appears at the bottom with a `working` line until the
|
|
|
|
| 252 |
|
| 253 |
| Today | Tomorrow |
|
| 254 |
|---|---|
|
| 255 |
+
| `Read this session's trace` on every agent row (`Sidebar.tsx:245`) | bottom bar → **reader** |
|
| 256 |
| `Share this session` on every agent row (`Sidebar.tsx:246`) | pane header → **share** |
|
| 257 |
| A `trace` **session row per read** (`App.tsx:openTrace`) | **gone** — no duplicate rows |
|
| 258 |
| Trace row's `Share` (`Sidebar.tsx:237`) | trace pane header (imported traces keep a pane) |
|
| 259 |
+
| Trace row's `Handover` (`Sidebar.tsx:238`) | reader / trace-pane footer |
|
| 260 |
| Quick-add **Trace** = open a shared dataset (`Sidebar.tsx:482`) | **stays** |
|
| 261 |
|
| 262 |
The last row is deliberate: an **imported** trace has no session behind it, so it is a genuine
|
|
|
|
| 378 |
|
| 379 |
1. **How far back in the card?** Proposal: one exchange per click, hard cap 5, then hand off to
|
| 380 |
the conversation. Alternative: one, then straight to the full conversation.
|
| 381 |
+
2. **Does the app open in reader mode**, or always start on the terminal?
|
| 382 |
3. **Kill the `↑ ↓` turn stepper?** Proposal: yes — unfolding replaces it.
|
| 383 |
4. **Unfolded card of a running agent**: live-refresh every 3 s, or freeze until it finishes
|
| 384 |
(cheaper, less alive)?
|
|
|
|
| 423 |
there is no transcript to read.
|
| 424 |
3. `pageOf()` takes a negative offset — "the last N turns" without a round trip to learn `total`
|
| 425 |
(`server/test/trace-tail.test.mjs`).
|
| 426 |
+
4. The session pane gets terminal ⇄ reader, from the bottom bar, app-wide. It draws over the terminal, which stays mounted and
|
| 427 |
connected but loses the keyboard; the mode is a per-session view preference in `localStorage`
|
| 428 |
(`web/src/lib/paneMode.ts`), and its event reaches a pane that is already open — which is what
|
| 429 |
the card's "full history ↗" needs.
|
|
|
|
| 435 |
6. `head.prompts[]` (§5) — index, first line and timestamp per prompt, so a surface can draw the
|
| 436 |
skeleton and label "show previous turn" before fetching the page that holds it.
|
| 437 |
7. The sidebar loses its trace buttons and `openTrace`; share moves into the pane header (§3.4).
|
| 438 |
+
8. Windowing by exchange in reader mode: a collapsed turn is 2–3 rows, so the DOM stays small,
|
| 439 |
`head.prompts[]` gives the skeleton up front, and only an opened turn needs its page. The
|
| 440 |
measured-height machinery in `TraceView` is reused as-is — what changes is what a "row" means.
|
| 441 |
Until then it reads the last 400 turns in one request.
|
web/src/App.tsx
CHANGED
|
@@ -79,11 +79,11 @@ export default function App() {
|
|
| 79 |
// stored — flipping the setting instantly (un)archives.
|
| 80 |
const [showArchived, setShowArchived] = useState(false);
|
| 81 |
const [archiveAfter, setArchiveAfter] = useState<'week' | 'month' | 'never'>('month');
|
| 82 |
-
// How every pane is read — the terminal itself, or
|
| 83 |
-
// App-wide, like zoom, and remembered the same way.
|
| 84 |
const [paneMode, setPaneMode] = useState(readPaneMode);
|
| 85 |
useEffect(() => onPaneMode(setPaneMode), []);
|
| 86 |
-
const showPaneMode = (m: 'terminal' | '
|
| 87 |
const [zoom, setZoom] = useState<number>(() => {
|
| 88 |
const z = parseInt(localStorage.getItem('am-zoom') || '100', 10);
|
| 89 |
return Number.isFinite(z) ? z : 100;
|
|
@@ -986,14 +986,14 @@ export default function App() {
|
|
| 986 |
</span>
|
| 987 |
)}
|
| 988 |
<span className="spacer" />
|
| 989 |
-
{/*
|
| 990 |
-
setting: how you are looking at everything, not what
|
| 991 |
-
|
| 992 |
<span className="seg modebar">
|
| 993 |
<button className={paneMode === 'terminal' ? 'on' : ''} title="The terminal itself"
|
| 994 |
onClick={() => showPaneMode('terminal')}>terminal</button>
|
| 995 |
-
<button className={paneMode === '
|
| 996 |
-
onClick={() => showPaneMode('
|
| 997 |
</span>
|
| 998 |
<button className="zbtn" title="Zoom out" onClick={() => setZoom((z) => Math.max(50, z - 10))}>−</button>
|
| 999 |
<button className="zlvl" title="Reset to 100%" onClick={() => setZoom(100)}>{zoom}%</button>
|
|
|
|
| 79 |
// stored — flipping the setting instantly (un)archives.
|
| 80 |
const [showArchived, setShowArchived] = useState(false);
|
| 81 |
const [archiveAfter, setArchiveAfter] = useState<'week' | 'month' | 'never'>('month');
|
| 82 |
+
// How every pane is read — the terminal itself, or reader mode over the same
|
| 83 |
+
// session. App-wide, like zoom, and remembered the same way.
|
| 84 |
const [paneMode, setPaneMode] = useState(readPaneMode);
|
| 85 |
useEffect(() => onPaneMode(setPaneMode), []);
|
| 86 |
+
const showPaneMode = (m: 'terminal' | 'reader') => { setPaneMode(m); writePaneMode(m); };
|
| 87 |
const [zoom, setZoom] = useState<number>(() => {
|
| 88 |
const z = parseInt(localStorage.getItem('am-zoom') || '100', 10);
|
| 89 |
return Number.isFinite(z) ? z : 100;
|
|
|
|
| 986 |
</span>
|
| 987 |
)}
|
| 988 |
<span className="spacer" />
|
| 989 |
+
{/* Reader mode sits with zoom because it is the same kind of
|
| 990 |
+
setting: how you are looking at everything, not what any one
|
| 991 |
+
pane is. The content is identical either way — this is form. */}
|
| 992 |
<span className="seg modebar">
|
| 993 |
<button className={paneMode === 'terminal' ? 'on' : ''} title="The terminal itself"
|
| 994 |
onClick={() => showPaneMode('terminal')}>terminal</button>
|
| 995 |
+
<button className={paneMode === 'reader' ? 'on' : ''} title="Reader mode — the same session, laid out"
|
| 996 |
+
onClick={() => showPaneMode('reader')}>reader</button>
|
| 997 |
</span>
|
| 998 |
<button className="zbtn" title="Zoom out" onClick={() => setZoom((z) => Math.max(50, z - 10))}>−</button>
|
| 999 |
<button className="zlvl" title="Reset to 100%" onClick={() => setZoom(100)}>{zoom}%</button>
|
web/src/components/Overview.tsx
CHANGED
|
@@ -72,9 +72,9 @@ function useConversationTail(id: string, on: boolean, live: boolean) {
|
|
| 72 |
return { turns, missing };
|
| 73 |
}
|
| 74 |
|
| 75 |
-
/** Opening this session's pane
|
| 76 |
const openRendered = (id: string, onOpen: (sid: string) => void) => {
|
| 77 |
-
writePaneMode('
|
| 78 |
onOpen(id);
|
| 79 |
};
|
| 80 |
|
|
|
|
| 72 |
return { turns, missing };
|
| 73 |
}
|
| 74 |
|
| 75 |
+
/** Opening this session's pane in reader mode rather than on the TTY. */
|
| 76 |
const openRendered = (id: string, onOpen: (sid: string) => void) => {
|
| 77 |
+
writePaneMode('reader'); // app-wide, and it reaches open panes too
|
| 78 |
onOpen(id);
|
| 79 |
};
|
| 80 |
|
web/src/components/TerminalPane.tsx
CHANGED
|
@@ -202,7 +202,7 @@ export default function TerminalPane({
|
|
| 202 |
// it — becoming active, the header, the key bar — and some fire after the mode
|
| 203 |
// changes, so the guard lives with the call rather than with the switch.
|
| 204 |
const modeRef = useRef<PaneMode>('terminal');
|
| 205 |
-
const focusTerm = () => { if (modeRef.current !== '
|
| 206 |
const frameRef = useRef<HTMLDivElement>(null);
|
| 207 |
const termRef = useRef<Terminal | null>(null);
|
| 208 |
const resyncRef = useRef<() => void>(() => {});
|
|
@@ -213,11 +213,11 @@ export default function TerminalPane({
|
|
| 213 |
const previousZoomRef = useRef(zoom);
|
| 214 |
const [preview] = useState<TerminalPreview | null>(() => loadTerminalPreview(session.id));
|
| 215 |
// The mode is app-wide (the bottom bar owns it, like zoom), but only an agent
|
| 216 |
-
// has a conversation to
|
| 217 |
// not this component's business at all.
|
| 218 |
const canRender = session.cli !== 'shell' && !isPassive(session.cli);
|
| 219 |
-
const reading = mode === '
|
| 220 |
-
modeRef.current = reading ? '
|
| 221 |
// Send a raw byte string to the PTY (for the mobile key-bar: arrows, Esc…).
|
| 222 |
const sendKeyRef = useRef<(d: string) => void>(() => {});
|
| 223 |
const [conn, setConn] = useState<ConnState>('connecting');
|
|
@@ -805,7 +805,7 @@ export default function TerminalPane({
|
|
| 805 |
return () => clearTimeout(t);
|
| 806 |
}, [active]);
|
| 807 |
|
| 808 |
-
//
|
| 809 |
// with focus swallows every keystroke into the agent's TTY, invisibly. Hand
|
| 810 |
// focus back when the terminal is on top again.
|
| 811 |
useEffect(() => {
|
|
@@ -862,11 +862,11 @@ export default function TerminalPane({
|
|
| 862 |
</div>
|
| 863 |
<div className="term-host" ref={frameRef}>
|
| 864 |
<div className="term-fill" ref={hostRef} />
|
| 865 |
-
{/*
|
| 866 |
layout to fit, and detaching tmux costs a repaint and can trip the
|
| 867 |
handoff path. The terminal stays mounted and connected underneath. */}
|
| 868 |
{reading && (
|
| 869 |
-
<div className="pane-
|
| 870 |
<ConversationView session={session} paused={visible === false} isMobile={isMobile} />
|
| 871 |
</div>
|
| 872 |
)}
|
|
|
|
| 202 |
// it — becoming active, the header, the key bar — and some fire after the mode
|
| 203 |
// changes, so the guard lives with the call rather than with the switch.
|
| 204 |
const modeRef = useRef<PaneMode>('terminal');
|
| 205 |
+
const focusTerm = () => { if (modeRef.current !== 'reader') termRef.current?.focus(); };
|
| 206 |
const frameRef = useRef<HTMLDivElement>(null);
|
| 207 |
const termRef = useRef<Terminal | null>(null);
|
| 208 |
const resyncRef = useRef<() => void>(() => {});
|
|
|
|
| 213 |
const previousZoomRef = useRef(zoom);
|
| 214 |
const [preview] = useState<TerminalPreview | null>(() => loadTerminalPreview(session.id));
|
| 215 |
// The mode is app-wide (the bottom bar owns it, like zoom), but only an agent
|
| 216 |
+
// has a conversation to read: a shell is a shell, and files/trace panels are
|
| 217 |
// not this component's business at all.
|
| 218 |
const canRender = session.cli !== 'shell' && !isPassive(session.cli);
|
| 219 |
+
const reading = mode === 'reader' && canRender;
|
| 220 |
+
modeRef.current = reading ? 'reader' : 'terminal';
|
| 221 |
// Send a raw byte string to the PTY (for the mobile key-bar: arrows, Esc…).
|
| 222 |
const sendKeyRef = useRef<(d: string) => void>(() => {});
|
| 223 |
const [conn, setConn] = useState<ConnState>('connecting');
|
|
|
|
| 805 |
return () => clearTimeout(t);
|
| 806 |
}, [active]);
|
| 807 |
|
| 808 |
+
// In reader mode the terminal is covered but still mounted — and a mounted xterm
|
| 809 |
// with focus swallows every keystroke into the agent's TTY, invisibly. Hand
|
| 810 |
// focus back when the terminal is on top again.
|
| 811 |
useEffect(() => {
|
|
|
|
| 862 |
</div>
|
| 863 |
<div className="term-host" ref={frameRef}>
|
| 864 |
<div className="term-fill" ref={hostRef} />
|
| 865 |
+
{/* Reader mode draws OVER the terminal rather than replacing it: xterm needs
|
| 866 |
layout to fit, and detaching tmux costs a repaint and can trip the
|
| 867 |
handoff path. The terminal stays mounted and connected underneath. */}
|
| 868 |
{reading && (
|
| 869 |
+
<div className="pane-reader" onMouseDown={(e) => e.stopPropagation()}>
|
| 870 |
<ConversationView session={session} paused={visible === false} isMobile={isMobile} />
|
| 871 |
</div>
|
| 872 |
)}
|
web/src/conversation.css
CHANGED
|
@@ -187,10 +187,10 @@ mark.cx-hit.on { background: var(--accent); color: var(--panel); }
|
|
| 187 |
.cs-detail { font-size: 12px; }
|
| 188 |
}
|
| 189 |
|
| 190 |
-
/* ----------
|
| 191 |
/* Drawn over the live terminal, which stays mounted underneath. */
|
| 192 |
-
.pane-
|
| 193 |
-
.pane-
|
| 194 |
.cxv-empty { margin: auto; padding: 24px; font-size: 11.5px; color: var(--muted); text-align: center; }
|
| 195 |
.ph-modes { flex: none; }
|
| 196 |
.ph-modes button { padding: 1px 7px; font-size: 9.5px; letter-spacing: 0.06em; }
|
|
|
|
| 187 |
.cs-detail { font-size: 12px; }
|
| 188 |
}
|
| 189 |
|
| 190 |
+
/* ---------- reader mode inside a session pane ---------- */
|
| 191 |
/* Drawn over the live terminal, which stays mounted underneath. */
|
| 192 |
+
.pane-reader { position: absolute; inset: 0; z-index: 3; display: flex; background: var(--panel); }
|
| 193 |
+
.pane-reader > .cxv { flex: 1; min-width: 0; }
|
| 194 |
.cxv-empty { margin: auto; padding: 24px; font-size: 11.5px; color: var(--muted); text-align: center; }
|
| 195 |
.ph-modes { flex: none; }
|
| 196 |
.ph-modes button { padding: 1px 7px; font-size: 9.5px; letter-spacing: 0.06em; }
|
web/src/lib/paneMode.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
| 1 |
-
// How every session pane is being read: the terminal, or
|
| 2 |
// (docs/conversation-view.md §3.3)
|
| 3 |
//
|
| 4 |
// One setting for the whole app, like zoom — not per session. Reading a fleet
|
| 5 |
// means reading it the same way; flipping panes one at a time was a preference
|
| 6 |
// nobody wanted to manage. Kept in localStorage so a reload does not undo it,
|
| 7 |
// and announced so an already-mounted pane hears about it.
|
| 8 |
-
export type PaneMode = 'terminal' | '
|
| 9 |
|
| 10 |
const KEY = 'am-pane-mode';
|
| 11 |
const EVENT = 'am:pane-mode';
|
| 12 |
|
| 13 |
export function readPaneMode(): PaneMode {
|
| 14 |
-
try { return localStorage.getItem(KEY) === '
|
| 15 |
}
|
| 16 |
|
| 17 |
export function writePaneMode(mode: PaneMode): void {
|
|
|
|
| 1 |
+
// How every session pane is being read: the terminal itself, or reader mode.
|
| 2 |
// (docs/conversation-view.md §3.3)
|
| 3 |
//
|
| 4 |
// One setting for the whole app, like zoom — not per session. Reading a fleet
|
| 5 |
// means reading it the same way; flipping panes one at a time was a preference
|
| 6 |
// nobody wanted to manage. Kept in localStorage so a reload does not undo it,
|
| 7 |
// and announced so an already-mounted pane hears about it.
|
| 8 |
+
export type PaneMode = 'terminal' | 'reader';
|
| 9 |
|
| 10 |
const KEY = 'am-pane-mode';
|
| 11 |
const EVENT = 'am:pane-mode';
|
| 12 |
|
| 13 |
export function readPaneMode(): PaneMode {
|
| 14 |
+
try { return localStorage.getItem(KEY) === 'reader' ? 'reader' : 'terminal'; } catch { return 'terminal'; }
|
| 15 |
}
|
| 16 |
|
| 17 |
export function writePaneMode(mode: PaneMode): void {
|