diff --git a/backlog/docs/doc-5 - Implementation-Analysis-Log-Line-Copy-in-Prod-Logs-Extension.md b/backlog/docs/doc-5 - Implementation-Analysis-Log-Line-Copy-in-Prod-Logs-Extension.md new file mode 100644 index 00000000..c271581e --- /dev/null +++ b/backlog/docs/doc-5 - Implementation-Analysis-Log-Line-Copy-in-Prod-Logs-Extension.md @@ -0,0 +1,141 @@ +--- +id: doc-5 +title: 'Implementation Analysis: Log Line Copy in Prod-Logs Extension' +type: other +created_date: '2026-05-03 21:06' +--- +# Implementation Analysis: Log Line Copy in Prod-Logs Extension + +## Problem Summary + +The `/prod-logs` extension (`.pi/extensions/prod-logs/index.ts`) displays log lines in a scrollable viewer but offers no way to copy lines. The user wants: + +1. Copy a single line (cursor line) and exit +2. Select and copy multiple lines and exit +3. Copied lines in ascending (chronological) order + +## Architecture Context + +- **Current component**: `LogViewer` class — manages scroll offset, renders lines, handles keyboard input +- **Current UI return type**: `ctx.ui.custom` — no value returned on close +- **Current key bindings**: Escape/q (close), r (refresh), Up/k Down/j (scroll), PgUp/PgDn (page), Home/End (jump) +- **Line ordering**: `logLines.reverse()` is called before display, so newest-first visually. Copied lines should be oldest-first (ascending). +- **No cursor concept exists** — only a scroll offset + +--- + +## Route A: Cursor + Toggle Selection + setEditorText + +### Concept +Add a visual cursor line. Enter to copy single line. Space to toggle line selection (multi-select mode). `y` to copy all selected lines and exit. After exit, call `ctx.ui.setEditorText()` with the copied text. + +### Changes to LogViewer +1. Add `cursorIndex: number` (absolute line index into `this.lines`) +2. Add `selectedIndices: Set` (absolute indices) +3. New key bindings: + - `Enter` → copy cursor line, `done(line)` + - `Space` → toggle selection of cursor line + - `y` → copy selected lines (or cursor line if none selected), `done(text)` +4. Render changes: + - `> ` prefix on cursor line (styled accent color) + - `● ` prefix on selected lines (styled success color) + - Non-selected, non-cursor lines keep existing ` ` prefix +5. Help text update: add "y copy · Space select · Enter copy line" + +### Return value handling +```typescript +const copiedText = await ctx.ui.custom(...); +if (copiedText !== null) { + ctx.ui.setEditorText(copiedText); +} +``` + +### Pros +- No external dependencies +- `setEditorText` is already available in pi's ExtensionAPI +- Aligns with the "paste" use case (text in editor = ready to send or copy) +- Simple, contained change to one file + +### Cons +- Modifies editor content (replaces whatever the user had typed) +- Not a true "system clipboard" copy — the user can't paste outside pi without an extra step +- Toggle-based selection for multi-line is somewhat unusual UX (vs. range selection) + +--- + +## Route B: Visual Mode (Vim-style) + setEditorText + +### Concept +Vim-inspired visual mode: `v` enters linewise visual selection, `j`/`k` extend the range, `y` copies and exits. Enter still copies single line. Selection is always a contiguous range. + +### Changes to LogViewer +1. Add `cursorIndex: number` (absolute) +2. Add `visualMode: boolean` + `visualAnchor: number` (start of visual selection) +3. New key bindings: + - `Enter` → copy cursor line, `done(line)` + - `v` → enter visual mode (set anchor to cursor) + - In visual mode: `j`/`k` extend selection range + - `y` (in visual mode) → copy range [min(anchor,cursor)..max(anchor,cursor)], `done(text)` + - `Escape` → exit visual mode +4. Render changes: + - Selected range lines get `● ` prefix (styled) + - Cursor line gets `> ` prefix +5. Help text: show visual mode help when active + +### Pros +- Familiar UX for vim users +- Contiguous range selection is intuitive +- Same `setEditorText` approach — no external deps + +### Cons +- Slightly more complex state machine (normal mode vs. visual mode) +- Doesn't allow non-contiguous selection (but the user said "contiguous") +- Still modifies editor content + +--- + +## Route C: System Clipboard + Either Selection Model + +### Concept +Either selection model (A or B), but instead of `setEditorText`, write to the system clipboard. Requires a package like `clipboardy`. + +### Additional Changes +- Add `clipboardy` dependency (or use `node:child_process` with `pbcopy`/`xclip`) +- On copy: `clipboardy.writeSync(text)` or similar +- No need to change return type — can stay `void` since we don't pass text back + +### Pros +- True "copy" behavior — paste anywhere +- Doesn't touch editor content +- Works across applications + +### Cons +- External dependency (`clipboardy` — native module, may have install issues) +- Platform-specific behavior (macOS `pbcopy`, Linux `xclip`/`wl-copy`, Windows) +- pi extensions run in the pi process; clipboard access may have security implications +- May not work in headless/server environments where pi runs + +--- + +## Recommendation: Route B (Visual Mode + setEditorText) + +**Rationale:** +1. **No external dependencies** — `setEditorText` is already available +2. **Familiar UX** — vim-style visual mode is intuitive for developers +3. **Contiguous selection** — matches the user's "contiguous lines" description +4. **Simple implementation** — contained change to one file, ~100 lines of code +5. **Immediate value** — text in editor can be sent as a prompt or manually copied + +**Why not clipboard (Route C):** The use case for `/prod-logs` is typically "I found an error line, I want to paste it into my next prompt to ask pi about it." `setEditorText` serves this perfectly. System clipboard adds complexity and platform fragility without proportional benefit for this specific use case. + +**Why not toggle selection (Route A):** The user said "contiguous lines." Toggle-based selection allows non-contiguous selection, which they don't need. Visual mode is simpler to implement and more conventional. + +--- + +## Outstanding Question for User + +How should the copied text be placed in the editor? +- **Append** to current editor content (preserves what's there) +- **Replace** current editor content (clean slate) + +My leaning: **Replace**, because the user's intent is to paste a log line as a new prompt. diff --git a/backlog/tasks/ml-159 - select-and-copy-log-lines-from-log-browser.md b/backlog/tasks/ml-159 - select-and-copy-log-lines-from-log-browser.md new file mode 100644 index 00000000..27fd242f --- /dev/null +++ b/backlog/tasks/ml-159 - select-and-copy-log-lines-from-log-browser.md @@ -0,0 +1,174 @@ +--- +id: ML-159 +title: select and copy log lines from log browser +status: To Do +assignee: [] +created_date: '2026-05-03 21:05' +updated_date: '2026-05-03 21:10' +labels: + - enhancement + - pi-extension + - prod-logs +dependencies: [] +references: + - .pi/extensions/prod-logs/index.ts +documentation: + - doc-5 (Implementation Analysis) +priority: medium +--- + +## Description + + +When using the `/prod-logs` extension (`.pi/extensions/prod-logs/index.ts`), add the ability to select and copy log lines: + +1. **Copy line under cursor** — The log browser currently has a scroll offset but no cursor concept. Add a visual cursor (highlighted line). On a keypress (e.g., Enter), copy that line, exit the log browser, and place the copied text where the user can paste it. + +2. **Select and copy multiple contiguous lines** — Add a selection mechanism. The user starts a selection, toggles lines on/off, then completes the selection. Copied lines should be in ascending order (oldest first — reversed compared to the visual display which shows newest first). Exit the log browser afterward so the copied lines can be pasted. + +The log browser currently uses `ctx.ui.custom`. To return copied text, the return type should change to `string | null` (null = cancelled/closed without copy). After the custom UI resolves, the copied text should be placed somewhere the user can immediately use (e.g., set in the editor, or copied to system clipboard). + + +## Acceptance Criteria + +- [ ] #1 Cursor line is visually highlighted with accent color and `> ` prefix +- [ ] #2 `v` enters visual mode; highlighted range extends as cursor moves +- [ ] #3 `Escape` exits visual mode and clears selection +- [ ] #4 `Enter` in normal mode copies the cursor line to editor and closes log browser +- [ ] #5 `y` in visual mode copies the selected range (oldest-first order) to editor and closes log browser +- [ ] #6 Copied text appears in the editor (via setEditorText) after log browser closes +- [ ] #7 Pressing Escape (without copying) closes the log browser without changing editor content +- [ ] #8 All existing key bindings (scroll, page, refresh, jump) continue to work in both normal and visual modes +- [ ] #9 Help text updates to show visual mode key bindings when visual mode is active +- [ ] #10 Empty or single-line log responses handle gracefully (no crash, sensible behavior) + + +## Implementation Plan + + +## Implementation Plan: Vim-style Visual Mode + setEditorText + +### 1. Add cursor state to LogViewer + +Add `cursorIndex` (absolute index into `this.lines`) and initialize to `0`. The cursor tracks which line is highlighted. When lines are refreshed, reset cursor to 0. + +**Verification**: After opening log browser, the first visible line should be highlighted with `> ` prefix and accent color. Scrolling should move the cursor with the viewport — cursor should remain on the top visible line after a page-down scroll (i.e., cursor should auto-clamp into the visible range). + +### 2. Add visual mode state to LogViewer + +Add `visualMode: boolean` and `visualAnchor: number`. `v` key enters visual mode at current cursor position. `Escape` exits visual mode (returns to normal mode). Normal scroll keys (`j`, `k`, `PgUp`, `PgDn`, etc.) continue to work in visual mode but also extend the selection range. + +**Verification**: Press `v` — visual mode indicator appears in help bar. Selection range highlights between anchor and cursor. Press Escape — visual mode clears, selection disappears. + +### 3. Update render to show cursor highlight and selection range + +- Cursor line: `> ` prefix with `theme.fg("accent", ...)` +- Selected lines (in range): `● ` prefix with `theme.fg("success", ...)` +- Lines that are both cursor AND in selection: `> ` prefix takes priority (cursor indicator) +- Non-cursor, non-selected: keep existing ` NNN │ ` prefix + +The line number column must shift right by 2 characters to accommodate the new prefix: ` NNN │ > text` instead of ` NNN │ text`. + +**Verification**: Visual inspection. Toggle visual mode, move cursor — see correct highlights. Exit visual mode — highlights disappear. + +### 4. Add copy key bindings + +- `Enter` (normal mode): Copy cursor line → `done(line)` (single line as string) +- `y` (visual mode): Copy range [min(anchor, cursor)..max(anchor, cursor)] joined with `\n` → `done(text)` +- Copied text must be in **ascending order** (oldest first). Since `this.lines` is already reversed before display, ascending means: take lines in range from `this.lines` in their stored order (which is reverse of visual order). Wait — check: `logLines.reverse()` is called before creating LogViewer, so `this.lines[0]` = newest, `this.lines[last]` = oldest. When we copy a range [a..b] where a < b, we get newest-first. To get oldest-first, we need to iterate `for (let i = end; i >= start; i--)` and join. + +Actually, confirm: `this.lines` has newest first (index 0 = newest). Visual order shows index 0 at top. So `this.lines[i]` with i ascending = newest to oldest. To get oldest-first: `this.lines.slice(start, end + 1).reverse().join("\n")`. + +**Verification**: Copy a range of lines. The resulting text should have oldest timestamp first, newest last. Use manual testing with logs that have visible timestamps. + +### 5. Change ctx.ui.custom return type from void to string | null + +```typescript +const copiedText = await ctx.ui.custom((tui, theme, _kb, done) => { + viewer.onCopy = (text: string) => done(text); + viewer.onClose = () => done(null); + // ... +}); + +if (copiedText !== null) { + ctx.ui.setEditorText(copiedText); +} +``` + +**Verification**: Copy a line → editor shows the copied text. Press Escape → editor remains unchanged (empty). + +### 6. Update help text + +Normal mode: `↑↓/jk scroll · PgUp/PgDn page · Home/End jump · v visual · Enter copy line · r refresh · Esc close` + +Visual mode: `VISUAL: j/k extend · y copy · Esc cancel · Enter copy line` + +**Verification**: Visual inspection of the help bar in both modes. + +### 7. Handle edge cases + +- **Empty log**: No copy operations allowed (cursor at -1 or lines.length === 0). Show "(no logs)". +- **Single line logs**: Visual mode works (range = just that one line). Enter copies it. +- **Cursor clamping**: After refresh, if cursor >= lines.length, clamp to lines.length - 1. +- **Visual mode across refresh**: Exit visual mode on refresh (lines change, anchor becomes stale). + +**Verification**: Test with empty API response, single-line response, and normal multi-line response. + +## Architecture Impact Analysis + +This is a **single-file, frontend-only change** to `.pi/extensions/prod-logs/index.ts`. No Elixir code, database schemas, contexts, PubSub topics, routes, or external API contracts are affected. + +### Touchpoints + +| Component | Impact | +|-----------|--------| +| `LogViewer` class | **Modified** — adds `cursorIndex`, `visualMode`, `visualAnchor`, `onCopy` callback. `handleInput` routing restructured for mode awareness | +| `LogViewer.render()` | **Modified** — adds cursor prefix (`> `) and selection prefix (`● `). Line number column padding widened by 2 chars | +| Extension handler function | **Modified** — `ctx.ui.custom` → `ctx.ui.custom`. Adds `onCopy` handler. Adds `ctx.ui.setEditorText()` on copy | +| Help text strings | **Modified** — split into normal-mode and visual-mode variants | +| `fetchLogs` / `updateLines` | **No change** — data fetching unchanged | +| Coolify API | **No change** — same endpoint, same auth | + +### Deprecation/Migration +None. Old behavior (scroll + refresh + close) is preserved and extended, not replaced. + +--- + +## Performance Profile + +- **Runtime complexity**: O(visibleHeight) per render — no change from current. Cursor and selection checks are O(1) per rendered line +- **Memory**: O(1) for cursor/anchor/visualMode fields; selection range is computed from two integers (no set storage) +- **Database queries**: None (client-side only) +- **N+1 risk**: None +- **Latency**: Zero latency added — all state changes are synchronous integer operations + +--- + +## Benchmarking Requirements + +**None required.** This is a purely interactive UI change with no database, network, or computational hot paths. The render path is identical to the existing code except for two integer comparisons per rendered line. + +--- + +## Cost Profile + +**No paid resources consumed.** The change does not add API calls, storage, compute, or third-party service usage beyond what already exists. + +--- + +## Production Infrastructure Steps + +**None.** No environment variables, service provisioning, database migrations, DNS changes, or firewall rules are needed. The extension is deployed as part of the pi extensions directory alongside the project. + +--- + +## Documentation Updates + +| File | Change Needed | +|------|--------------| +| `docs/architecture.md` | **No change** — this is a pi extension, not part of the Elixir application architecture | +| `docs/project-conventions.md` | **No change** — TypeScript conventions are pi's domain, not the project's | +| `docs/production-infrastructure.md` | **No change** — no infra changes | +| `docs/available-tasks.md` | **No change** — no new mise tasks | +| `.pi/extensions/prod-logs/index.ts` | **Inline comments** — add JSDoc on new fields (`cursorIndex`, `visualMode`, `visualAnchor`, `onCopy`) and new key binding branches | +