opentui(copy): /copy [n] copies the agent response
This commit is contained in:
parent
0f53d67ee4
commit
028bd89959
@ -31,6 +31,7 @@ import { liveGatewayLayer } from '../boundary/gateway/liveGateway.ts'
|
|||||||
import { getLog } from '../boundary/log.ts'
|
import { getLog } from '../boundary/log.ts'
|
||||||
import { acquireRenderer } from '../boundary/renderer.ts'
|
import { acquireRenderer } from '../boundary/renderer.ts'
|
||||||
import { makeAppLayer } from '../boundary/runtime.ts'
|
import { makeAppLayer } from '../boundary/runtime.ts'
|
||||||
|
import { nthAssistantResponse } from '../logic/copy.ts'
|
||||||
import { createPromptHistory, dirHistoryPersister, loadDirHistory } from '../logic/history.ts'
|
import { createPromptHistory, dirHistoryPersister, loadDirHistory } from '../logic/history.ts'
|
||||||
import { createPasteStore } from '../logic/pastes.ts'
|
import { createPasteStore } from '../logic/pastes.ts'
|
||||||
import { mapResumeHistory, mapSessionList } from '../logic/resume.ts'
|
import { mapResumeHistory, mapSessionList } from '../logic/resume.ts'
|
||||||
@ -319,6 +320,13 @@ export const run = Effect.fn('Tui.run')(function* (input: TuiInput) {
|
|||||||
const slashCtx: SlashContext = {
|
const slashCtx: SlashContext = {
|
||||||
clearTranscript: () => store.clearTranscript(),
|
clearTranscript: () => store.clearTranscript(),
|
||||||
confirm: (message, onConfirm) => store.setConfirm(message, onConfirm),
|
confirm: (message, onConfirm) => store.setConfirm(message, onConfirm),
|
||||||
|
copyResponse: n => {
|
||||||
|
const text = nthAssistantResponse(store.state.messages, n)
|
||||||
|
if (!text) return false
|
||||||
|
void writeClipboard(text)
|
||||||
|
flashHint(n > 1 ? `Copied response #${n} to clipboard` : 'Copied response to clipboard')
|
||||||
|
return true
|
||||||
|
},
|
||||||
listSessions: () => Effect.runPromise(gateway.request('session.list', {})).then(mapSessionList),
|
listSessions: () => Effect.runPromise(gateway.request('session.list', {})).then(mapSessionList),
|
||||||
logTail: () =>
|
logTail: () =>
|
||||||
getLog()
|
getLog()
|
||||||
|
|||||||
@ -40,6 +40,8 @@ export interface SlashContext {
|
|||||||
/** Open a local Y/N confirm; `onConfirm` runs on Yes. */
|
/** Open a local Y/N confirm; `onConfirm` runs on Yes. */
|
||||||
readonly confirm: (message: string, onConfirm: () => void) => void
|
readonly confirm: (message: string, onConfirm: () => void) => void
|
||||||
readonly clearTranscript: () => void
|
readonly clearTranscript: () => void
|
||||||
|
/** Copy the n-th newest assistant response to the clipboard; returns whether something was copied. */
|
||||||
|
readonly copyResponse: (n: number) => boolean
|
||||||
readonly quit: () => void
|
readonly quit: () => void
|
||||||
/** Recent log lines for `/logs` (the ring buffer). */
|
/** Recent log lines for `/logs` (the ring buffer). */
|
||||||
readonly logTail: () => string[]
|
readonly logTail: () => string[]
|
||||||
@ -133,6 +135,7 @@ function present(ctx: SlashContext, title: string, text: string): void {
|
|||||||
const CLIENT_HELP = [
|
const CLIENT_HELP = [
|
||||||
'/help — list commands',
|
'/help — list commands',
|
||||||
'/model [name] — switch model (picker if bare)',
|
'/model [name] — switch model (picker if bare)',
|
||||||
|
'/copy [n] — copy the last (or n-th) response',
|
||||||
'/skills — browse skills',
|
'/skills — browse skills',
|
||||||
'/sessions, /resume — switch/resume a session',
|
'/sessions, /resume — switch/resume a session',
|
||||||
'/clear, /new — clear the transcript (confirm)',
|
'/clear, /new — clear the transcript (confirm)',
|
||||||
@ -239,6 +242,10 @@ const toolsCmd: ClientHandler = async (arg, ctx) => {
|
|||||||
const CLIENT: Record<string, ClientHandler> = {
|
const CLIENT: Record<string, ClientHandler> = {
|
||||||
agents: (_arg, ctx) => ctx.openDashboard(),
|
agents: (_arg, ctx) => ctx.openDashboard(),
|
||||||
clear: (_arg, ctx) => ctx.confirm('Clear the transcript?', ctx.clearTranscript),
|
clear: (_arg, ctx) => ctx.confirm('Clear the transcript?', ctx.clearTranscript),
|
||||||
|
copy: (arg, ctx) => {
|
||||||
|
const n = Math.max(1, Number.parseInt(arg, 10) || 1)
|
||||||
|
if (!ctx.copyResponse(n)) ctx.pushSystem('Nothing to copy yet.')
|
||||||
|
},
|
||||||
exit: (_arg, ctx) => ctx.quit(),
|
exit: (_arg, ctx) => ctx.quit(),
|
||||||
model: modelCmd,
|
model: modelCmd,
|
||||||
resume: openSwitcher,
|
resume: openSwitcher,
|
||||||
|
|||||||
@ -91,6 +91,8 @@ interface Probe {
|
|||||||
quit: { value: boolean }
|
quit: { value: boolean }
|
||||||
cleared: { value: boolean }
|
cleared: { value: boolean }
|
||||||
dashboard: { value: boolean }
|
dashboard: { value: boolean }
|
||||||
|
copied: number[]
|
||||||
|
copyN: { value: (n: number) => boolean }
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCtx(request: (method: string, params: Record<string, unknown>) => Promise<unknown>): Probe {
|
function makeCtx(request: (method: string, params: Record<string, unknown>) => Promise<unknown>): Probe {
|
||||||
@ -104,9 +106,15 @@ function makeCtx(request: (method: string, params: Record<string, unknown>) => P
|
|||||||
const quit = { value: false }
|
const quit = { value: false }
|
||||||
const cleared = { value: false }
|
const cleared = { value: false }
|
||||||
const dashboard = { value: false }
|
const dashboard = { value: false }
|
||||||
|
const copied: number[] = []
|
||||||
|
const copyN: Probe['copyN'] = { value: () => false }
|
||||||
const ctx: SlashContext = {
|
const ctx: SlashContext = {
|
||||||
clearTranscript: () => (cleared.value = true),
|
clearTranscript: () => (cleared.value = true),
|
||||||
confirm: (message, onConfirm) => confirmed.push({ message, onConfirm }),
|
confirm: (message, onConfirm) => confirmed.push({ message, onConfirm }),
|
||||||
|
copyResponse: n => {
|
||||||
|
copied.push(n)
|
||||||
|
return copyN.value(n)
|
||||||
|
},
|
||||||
listSessions: () => Promise.resolve(FAKE_SESSIONS),
|
listSessions: () => Promise.resolve(FAKE_SESSIONS),
|
||||||
logTail: () => ['gateway: spawned', 'bootstrap: session created'],
|
logTail: () => ['gateway: spawned', 'bootstrap: session created'],
|
||||||
openDashboard: () => (dashboard.value = true),
|
openDashboard: () => (dashboard.value = true),
|
||||||
@ -122,7 +130,21 @@ function makeCtx(request: (method: string, params: Record<string, unknown>) => P
|
|||||||
sessionId: () => 'sid-1',
|
sessionId: () => 'sid-1',
|
||||||
submit: text => submitted.push(text)
|
submit: text => submitted.push(text)
|
||||||
}
|
}
|
||||||
return { calls, cleared, confirmed, ctx, dashboard, paged, pickers, quit, submitted, switched, system }
|
return {
|
||||||
|
calls,
|
||||||
|
cleared,
|
||||||
|
confirmed,
|
||||||
|
copied,
|
||||||
|
copyN,
|
||||||
|
ctx,
|
||||||
|
dashboard,
|
||||||
|
paged,
|
||||||
|
pickers,
|
||||||
|
quit,
|
||||||
|
submitted,
|
||||||
|
switched,
|
||||||
|
system
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('dispatchSlash — client commands', () => {
|
describe('dispatchSlash — client commands', () => {
|
||||||
@ -198,6 +220,28 @@ describe('dispatchSlash — client commands', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('/copy copies via copyResponse; no system line on success', async () => {
|
||||||
|
const p = makeCtx(async () => ({}))
|
||||||
|
p.copyN.value = () => true
|
||||||
|
await dispatchSlash('/copy', p.ctx)
|
||||||
|
expect(p.copied).toEqual([1])
|
||||||
|
expect(p.system).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('/copy 2 passes the n-th index through', async () => {
|
||||||
|
const p = makeCtx(async () => ({}))
|
||||||
|
p.copyN.value = () => true
|
||||||
|
await dispatchSlash('/copy 2', p.ctx)
|
||||||
|
expect(p.copied).toEqual([2])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('/copy when nothing to copy pushes a system notice', async () => {
|
||||||
|
const p = makeCtx(async () => ({}))
|
||||||
|
p.copyN.value = () => false
|
||||||
|
await dispatchSlash('/copy', p.ctx)
|
||||||
|
expect(p.system).toContain('Nothing to copy yet.')
|
||||||
|
})
|
||||||
|
|
||||||
test('/agents (and /tasks) open the agents dashboard', async () => {
|
test('/agents (and /tasks) open the agents dashboard', async () => {
|
||||||
const p = makeCtx(async () => ({}))
|
const p = makeCtx(async () => ({}))
|
||||||
await dispatchSlash('/agents', p.ctx)
|
await dispatchSlash('/agents', p.ctx)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user