clauderipple 0.9.4 → 0.10.0
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/CHANGELOG.md +80 -0
- package/README.md +31 -0
- package/dist/app/package.json +1 -1
- package/dist/cli/src/hooks/agent-title.js +7 -4
- package/dist/cli/src/index.js +43 -0
- package/dist/cli/src/remote.js +263 -0
- package/dist/cli/src/schtasks.js +48 -40
- package/dist/cli/src/settings.js +2 -1
- package/dist/mod/hooks/i18n.ts +252 -6
- package/dist/mod/hooks/judge.ts +35 -17
- package/dist/mod/hooks/modes.ts +56 -1
- package/dist/mod/hooks/register.tsx +238 -32
- package/dist/mod/hooks/session.ts +98 -1
- package/dist/mod/hooks/settings.js +7 -3
- package/dist/mod/hooks/settings.ts +9 -4
- package/dist/mod/hooks/stats.ts +25 -1
- package/dist/mod/hooks/ui/agents.tsx +858 -79
- package/dist/mod/hooks/ui/alerts.tsx +20 -17
- package/dist/mod/hooks/ui/art.ts +33 -7
- package/dist/mod/hooks/ui/band.tsx +262 -81
- package/dist/mod/hooks/ui/flat.ts +265 -0
- package/dist/mod/hooks/ui/kit.tsx +70 -55
- package/dist/mod/hooks/ui/look.ts +129 -0
- package/dist/mod/hooks/ui/settings.tsx +71 -46
- package/dist/mod/hooks/ui/setup.tsx +15 -13
- package/dist/mod/hooks/ui/theme.ts +3 -0
- package/dist/mod/hooks/ui/tools.ts +124 -0
- package/dist/mod/hooks/ui/view.ts +116 -3
- package/dist/mod/hooks/workers.ts +33 -6
- package/dist/mod/tests/register.test.tsx +155 -11
- package/dist/mod/tests/ui.test.tsx +375 -27
- package/dist/mod/types/index.d.ts +68 -2
- package/dist/remote/package.json +28 -0
- package/dist/remote/src/away.js +85 -0
- package/dist/remote/src/bridge-client.js +295 -0
- package/dist/remote/src/composer-menu.js +11 -0
- package/dist/remote/src/config.js +102 -0
- package/dist/remote/src/desktop.js +1018 -0
- package/dist/remote/src/e2ee.js +126 -0
- package/dist/remote/src/e2ee.ts +126 -0
- package/dist/remote/src/eventlog.js +298 -0
- package/dist/remote/src/expo-push.js +127 -0
- package/dist/remote/src/folders.js +79 -0
- package/dist/remote/src/fsutil.js +39 -0
- package/dist/remote/src/handoff.js +38 -0
- package/dist/remote/src/host-service.js +168 -0
- package/dist/remote/src/images.js +215 -0
- package/dist/remote/src/local-control.js +138 -0
- package/dist/remote/src/main.js +264 -0
- package/dist/remote/src/permissions.js +124 -0
- package/dist/remote/src/power.js +76 -0
- package/dist/remote/src/processes.js +24 -0
- package/dist/remote/src/protocol.js +32 -0
- package/dist/remote/src/protocol.ts +284 -0
- package/dist/remote/src/push-content.js +32 -0
- package/dist/remote/src/push.js +134 -0
- package/dist/remote/src/receipts.js +111 -0
- package/dist/remote/src/relay.js +438 -0
- package/dist/remote/src/router.js +208 -0
- package/dist/remote/src/sdk-launcher.js +18 -0
- package/dist/remote/src/server.js +706 -0
- package/dist/remote/src/service.js +157 -0
- package/dist/remote/src/sessions.js +1800 -0
- package/dist/remote/src/sidebar-storage.js +211 -0
- package/dist/remote/src/sidebar.js +173 -0
- package/dist/remote/src/tool-details.js +205 -0
- package/dist/remote/src/tool-details.ts +153 -0
- package/dist/remote/src/worktree.js +99 -0
- package/dist/remote/web/app.ts +746 -0
- package/dist/remote/web/conn.ts +193 -0
- package/dist/remote/web/demo.ts +134 -0
- package/dist/remote/web/dom.ts +124 -0
- package/dist/remote/web/esc.ts +4 -0
- package/dist/remote/web/i18n.ts +374 -0
- package/dist/remote/web/icon.svg +1 -0
- package/dist/remote/web/index.html +24 -0
- package/dist/remote/web/manifest.webmanifest +10 -0
- package/dist/remote/web/mascot.ts +60 -0
- package/dist/remote/web/md.ts +78 -0
- package/dist/remote/web/notify.ts +55 -0
- package/dist/remote/web/pushstate.ts +27 -0
- package/dist/remote/web/secure.ts +109 -0
- package/dist/remote/web/sidebar.ts +106 -0
- package/dist/remote/web/style.css +311 -0
- package/dist/remote/web/sw.ts +48 -0
- package/dist/remote/web/timeline.ts +224 -0
- package/dist/remote/web/views.ts +323 -0
- package/dist/router/src/admin.js +227 -4
- package/dist/router/src/config.js +30 -0
- package/dist/router/src/index.js +39 -0
- package/dist/router/src/ingress/translate.js +6 -2
- package/dist/router/src/phone.js +82 -0
- package/dist/router/src/presets.js +3 -0
- package/dist/router/src/providers/anthropic-usage.js +33 -1
- package/dist/router/src/providers/chatgpt/index.js +41 -11
- package/dist/router/src/providers/chatgpt/translate.js +147 -7
- package/dist/router/src/providers/google/index.js +26 -0
- package/dist/router/src/providers/google/prompt-refusal.js +29 -0
- package/dist/router/src/providers/google/usage.js +257 -0
- package/dist/router/src/providers/grok/index.js +18 -0
- package/dist/router/src/providers/grok/usage.js +77 -0
- package/dist/router/src/providers/openai/index.js +29 -5
- package/dist/router/src/providers/openai/translate.js +260 -27
- package/dist/router/src/providers/opencode-usage.js +116 -0
- package/dist/router/src/proxy.js +339 -258
- package/dist/router/src/reasoning.js +157 -0
- package/dist/router/src/remote-host.js +320 -0
- package/dist/router/src/remote-tunnel.js +320 -0
- package/dist/router/src/requestlog.js +48 -39
- package/dist/router/src/thinking.js +20 -9
- package/dist/router/src/usage-history.js +227 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +436 -59
- package/dist/ui/i18n.js +184 -0
- package/dist/ui/index.html +24 -5
- package/dist/ui/style.css +39 -0
- package/docs/ARCHITECTURE.md +164 -12
- package/package.json +7 -1
|
@@ -9,10 +9,12 @@ import { expect, test } from 'claude-code/testing'
|
|
|
9
9
|
import type { Engine } from 'claude-code/testing'
|
|
10
10
|
import type { On, RenderElement, RenderPropsOf } from 'claude-code'
|
|
11
11
|
|
|
12
|
-
import type { AgentRec, Settings, StatusRequest } from '../types'
|
|
12
|
+
import type { AgentRec, Settings, Skin, StatusRequest } from '../types'
|
|
13
13
|
import { STRINGS } from '../hooks/i18n'
|
|
14
14
|
import { normalizeSettings } from '../hooks/settings'
|
|
15
|
-
import { renderAgents } from '../hooks/ui/agents'
|
|
15
|
+
import { cardRows, renderAgents, talkOf, timeCells } from '../hooks/ui/agents'
|
|
16
|
+
import { chOf, displayWidth } from '../hooks/ui/theme'
|
|
17
|
+
import { commandLine, runSummary, shortPath, toolDetail } from '../hooks/ui/tools'
|
|
16
18
|
import { renderAlert } from '../hooks/ui/alerts'
|
|
17
19
|
import { BAND_RIPPLE, CREST_CELLS, bandMood, besideBand, modeLabel, renderBand, renderFooter } from '../hooks/ui/band'
|
|
18
20
|
import { rippleSource } from '../hooks/ui/parts'
|
|
@@ -20,7 +22,7 @@ import { renderLog } from '../hooks/ui/log'
|
|
|
20
22
|
import { renderSettings } from '../hooks/ui/settings'
|
|
21
23
|
import { renderSetup } from '../hooks/ui/setup'
|
|
22
24
|
import { countdown, elapsed, minutesLeft, modelName, nextMinuteChange, resetsLabel, routeOf, seconds, targetOf, targetValue } from '../hooks/ui/view'
|
|
23
|
-
import type { AlertView, DashboardView, Frame, JevState, SettingsActions, Surface } from '../hooks/ui/view'
|
|
25
|
+
import type { AgentEntry, AgentsActions, AlertView, DashboardView, Frame, JevState, SettingsActions, Surface } from '../hooks/ui/view'
|
|
24
26
|
|
|
25
27
|
const SURFACES = ['terminal', 'desktop'] as const
|
|
26
28
|
const NOW = Date.parse('2026-10-10T13:20:00')
|
|
@@ -30,8 +32,8 @@ function paneProps(columns: number, rows = 30): RenderPropsOf['Pane'] {
|
|
|
30
32
|
return { title: 'ui', isFocused: true, bodyColumns: columns, placement: 'inline', scroll: { offset: 0, bodyRows: rows }, view: {} } as RenderPropsOf['Pane']
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
function frameOf(surface: Surface, columns = 140, maxRows = 12): Frame {
|
|
34
|
-
return { surface, columns, maxRows, now: NOW, appearance: 'auto', lang: 'en' }
|
|
35
|
+
function frameOf(surface: Surface, columns = 140, maxRows = 12, skin: Skin = 'deep'): Frame {
|
|
36
|
+
return { surface, columns, maxRows, now: NOW, appearance: 'auto', skin, lang: 'en' }
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/** Records which callback ran, with what. */
|
|
@@ -107,6 +109,8 @@ function mount($: Engine, id: string, surface: (typeof SURFACES)[number], column
|
|
|
107
109
|
test('view helpers', () => {
|
|
108
110
|
expect(modelName('claude-opus-5-5[1m]')).toBe('Opus 5.5')
|
|
109
111
|
expect(modelName('claude-fable-5-1')).toBe('Fable 5.1')
|
|
112
|
+
expect(modelName('claude-mythos-5-1')).toBe('Mythos 5.1')
|
|
113
|
+
expect(modelName('mythos')).toBe('Mythos')
|
|
110
114
|
expect(modelName('sonnet')).toBe('Sonnet')
|
|
111
115
|
expect(modelName('gpt-6.1-sol')).toBe('gpt-6.1-sol')
|
|
112
116
|
expect(countdown(41 * 60_000, 'en')).toBe('41m')
|
|
@@ -177,7 +181,8 @@ test('the band draws on both surfaces and its controls reach their callbacks', a
|
|
|
177
181
|
expect(await ui.find({ type: 'Text', text: /^Keep cache warm Off · / })).toBeDefined()
|
|
178
182
|
// What captions said is in the hover tips.
|
|
179
183
|
expect(await ui.find({ type: 'Text', text: /^Opus 5\.5 · Auto · Effort High — multi-file refactor/ })).toBeDefined()
|
|
180
|
-
|
|
184
|
+
// The route has no tip of its own: the log it opens is its card (028aee5).
|
|
185
|
+
expect(await ui.find({ type: 'Text', text: /^Last route: / })).toBeUndefined()
|
|
181
186
|
// The route opens the log too; the effort its menu.
|
|
182
187
|
await ui.press({ key: 'route' })
|
|
183
188
|
await ui.press({ key: 'effort' })
|
|
@@ -383,8 +388,8 @@ test('리플이 greets in setup and quick settings and waits in empty panes, nev
|
|
|
383
388
|
drawPane(on, 'm-setup-1', (el, surface) => renderSetup(el, frameOf(surface), { step: 1, draft: SETTINGS, jev: { source: 'env', test: { state: 'idle' } } }, setupActions))
|
|
384
389
|
drawPane(on, 'm-setup-4', (el, surface) => renderSetup(el, frameOf(surface), { step: 4, draft: SETTINGS, jev: { source: 'env', test: { state: 'idle' } } }, setupActions))
|
|
385
390
|
drawPane(on, 'm-quick', (el, surface) => renderSettings(el, frameOf(surface), { settings: SETTINGS, jev: { source: null, test: { state: 'idle' } } }, QUICK_ACTIONS))
|
|
386
|
-
drawPane(on, 'm-agents', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: AGENTS, now: NOW, showDone: true }, { toggleDone: noop }))
|
|
387
|
-
drawPane(on, 'm-agents-empty', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: [], now: NOW, showDone: false }, { toggleDone: noop }))
|
|
391
|
+
drawPane(on, 'm-agents', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: AGENTS, now: NOW, showDone: true }, { ...AGENT_NOOP, toggleDone: noop }))
|
|
392
|
+
drawPane(on, 'm-agents-empty', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: [], now: NOW, showDone: false }, { ...AGENT_NOOP, toggleDone: noop }))
|
|
388
393
|
drawPane(on, 'm-log-empty', (el, surface) => renderLog(el, frameOf(surface, 64), { rows: [], mineOnly: true }, { toggleScope: noop }))
|
|
389
394
|
// It greets and waits at 56 px, and heads the quick settings at 48.
|
|
390
395
|
const expected: [string, number, string, number][] = [['m-setup-1', 1, 'var(--accent)', 56], ['m-setup-4', 1, 'var(--ok)', 56], ['m-quick', 1, 'var(--accent)', 48], ['m-agents', 0, '', 0], ['m-agents-empty', 1, 'var(--accent)', 56], ['m-log-empty', 1, 'var(--accent)', 56]]
|
|
@@ -455,6 +460,61 @@ test('quick settings controls reach their callbacks on both surfaces', async ($,
|
|
|
455
460
|
expect(calls).toEqual([...once, ...once])
|
|
456
461
|
})
|
|
457
462
|
|
|
463
|
+
// ---- Skins (look.ts) ---------------------------------------------------------------------------
|
|
464
|
+
|
|
465
|
+
const SKINS: readonly Skin[] = ['deep', 'flat']
|
|
466
|
+
|
|
467
|
+
test('the skin tile offers deep and flat on the desktop alone, and a pick patches the setting', async ($, on) => {
|
|
468
|
+
const { calls, fn } = recorder()
|
|
469
|
+
const actions = { ...QUICK_ACTIONS, patch: fn('patch') }
|
|
470
|
+
drawPane(on, 'skin', (el, surface) => renderSettings(el, frameOf(surface), { settings: SETTINGS, jev: { source: null, test: { state: 'idle' } } }, actions))
|
|
471
|
+
const desktop = await mount($, 'skin', 'desktop')
|
|
472
|
+
for (const skin of SKINS) expect(await desktop.find({ key: 'skin-' + skin }), skin).toBeDefined()
|
|
473
|
+
expect(await desktop.find({ type: 'Text', text: 'Skin' })).toBeDefined()
|
|
474
|
+
await desktop.press({ key: 'skin-flat' })
|
|
475
|
+
await desktop.unmount()
|
|
476
|
+
// The terminal draws text whatever the skin, so it does not offer one.
|
|
477
|
+
const terminal = await mount($, 'skin', 'terminal')
|
|
478
|
+
for (const skin of SKINS) expect(await terminal.find({ key: 'skin-' + skin })).toBeUndefined()
|
|
479
|
+
await terminal.unmount()
|
|
480
|
+
expect(calls).toEqual(['patch:[{"skin":"flat"}]'])
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
/** The deep's three inks for host Text (art.ts): a skin that is not the deep uses none of them. */
|
|
484
|
+
const DEEP_INKS = ['#f2f0eb', '#cbc8bf', '#9c988e']
|
|
485
|
+
|
|
486
|
+
test('every skin draws the band, the quick settings and a card on the desktop; flat paints the app\'s surface and its quiet words', async ($, on) => {
|
|
487
|
+
const noop = () => undefined
|
|
488
|
+
const bandActions = { toggleAuto: noop, toggleWarm: noop, compact: noop, clear: noop, handoff: noop, toggleLog: noop, toggleAgents: noop, openSettings: noop, toggleEffortMenu: noop, pickEffort: noop, hide: noop }
|
|
489
|
+
const alertActions = { compact: noop, clear: noop, handoff: noop, dismiss: noop, toggleSave: noop, openSettings: noop, noteInput: noop, setHandoffKind: noop, setHandoffAfter: noop, go: noop, copy: noop, openNew: noop }
|
|
490
|
+
const full: DashboardView = { ...DASH, extras: ['context', 'limits'], limit: { kind: 'five_hour', percent: 42, save: true }, handoff: 'advised', effortMenu: true, warm: { on: true, pings: 2, cost: 9000 }, agents: { live: 2, total: 3 }, mode: 'Burn GPT' }
|
|
491
|
+
for (const skin of SKINS) {
|
|
492
|
+
drawPane(on, skin + '-band', el => renderBand(el, frameOf('desktop', 100, 12, skin), full, bandActions))
|
|
493
|
+
drawPane(on, skin + '-quick', el => renderSettings(el, frameOf('desktop', 140, 12, skin), { settings: { ...SETTINGS, skin, jevSource: 'typesafe' }, jev: { source: null, test: { state: 'fail', message: 'no' } } }, QUICK_ACTIONS))
|
|
494
|
+
drawPane(on, skin + '-card', el => renderAlert(el, frameOf('desktop', 100, 12, skin), { kind: 'handoffChoice', handoffKind: 'quick', after: 'continue' }, alertActions))
|
|
495
|
+
drawPane(on, skin + '-setup', el => renderSetup(el, frameOf('desktop', 100, 12, skin), { step: 1, draft: SETTINGS, jev: { source: null, test: { state: 'idle' } } }, { patch: noop, jevKey: noop, testJev: noop, openAllSettings: noop, back: noop, next: noop, finish: noop, skip: noop }))
|
|
496
|
+
}
|
|
497
|
+
for (const skin of SKINS) {
|
|
498
|
+
for (const what of ['band', 'quick', 'card', 'setup']) {
|
|
499
|
+
const ui = await mount($, skin + '-' + what, 'desktop')
|
|
500
|
+
const svgs = await ui.findAll({ type: 'Svg' })
|
|
501
|
+
expect(svgs.length, skin + ' ' + what + ' pictures').toBeGreaterThan(3)
|
|
502
|
+
for (const svg of svgs) expect(String(svg.props.source), skin + ' ' + what + ' ' + svg.props.alt).not.toMatch(/NaN|undefined/)
|
|
503
|
+
// The capsule's backdrop: a picture as wide as the frame (flat's is the app's own surface).
|
|
504
|
+
const backdrop = svgs.some(svg => svg.props.alt === 'ClaudeRipple' && Number(svg.props.width) > 300)
|
|
505
|
+
expect(backdrop, skin + ' ' + what + ' backdrop').toBe(true)
|
|
506
|
+
const colours = new Set((await ui.findAll({ type: 'Text' })).map(text => String(text.props.color ?? '')))
|
|
507
|
+
if (skin === 'deep') expect(DEEP_INKS.some(ink => colours.has(ink)), skin + ' ' + what + ' ink').toBe(true)
|
|
508
|
+
else for (const ink of DEEP_INKS) expect(colours.has(ink), skin + ' ' + what + ' ' + ink).toBe(false)
|
|
509
|
+
if (skin === 'flat') {
|
|
510
|
+
// The host's ink for values, the app's quiet text colour for the names and asides.
|
|
511
|
+
expect(['#73726c', '#9c9a92', '#86847d'].some(c => colours.has(c)), skin + ' ' + what + ' quiet words').toBe(true)
|
|
512
|
+
}
|
|
513
|
+
await ui.unmount()
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
|
|
458
518
|
test('Jev key and Test row appear only for TypeSafe Jev with no known key', async ($, on) => {
|
|
459
519
|
const sources: JevState['source'][] = [null, 'zen', 'env', 'file', 'pasted']
|
|
460
520
|
for (const judge of ['haiku', 'jev', 'model'] as const) {
|
|
@@ -517,30 +577,144 @@ const AGENTS: AgentRec[] = [
|
|
|
517
577
|
toolSince: NOW - 14_000,
|
|
518
578
|
startedAt: NOW - 192_000,
|
|
519
579
|
cost: 38_400,
|
|
580
|
+
provider: 'chatgpt',
|
|
581
|
+
effort: 'high',
|
|
582
|
+
answeredBy: 'gpt-6.1-sol',
|
|
583
|
+
usage: { input: 1200, cached: 38_000, cacheWrite: 800, output: 5100 },
|
|
584
|
+
tools: 12,
|
|
520
585
|
},
|
|
521
586
|
{ id: 'a2', description: 'Summarize the docs', type: 'deepseek-v4-1-flash', state: 'done', startedAt: NOW - 900_000, endedAt: NOW - 800_000, cost: 9000 },
|
|
522
587
|
]
|
|
523
588
|
|
|
589
|
+
const AGENT_NOOP: AgentsActions = { toggleDone: () => undefined, open: () => undefined, back: () => undefined, earlier: () => undefined, toggleRun: () => undefined }
|
|
590
|
+
|
|
591
|
+
test('the Agents pane draws a worker the session\'s own model runs', async ($, on) => {
|
|
592
|
+
const onSession: AgentRec = { ...AGENTS[0]!, id: 's1', description: 'Review on the session model', pick: { ...AGENTS[0]!.pick!, target: { agent: 'general-purpose' }, by: 'session' } }
|
|
593
|
+
drawPane(on, 'agents-session', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: [onSession], now: NOW, showDone: false }, { ...AGENT_NOOP, toggleDone: () => undefined }))
|
|
594
|
+
for (const surface of SURFACES) {
|
|
595
|
+
const ui = await mount($, 'agents-session', surface, 64)
|
|
596
|
+
expect(await ui.find({ type: 'Text', text: 'Review on the session model' }), surface).toBeDefined()
|
|
597
|
+
await ui.unmount()
|
|
598
|
+
}
|
|
599
|
+
})
|
|
600
|
+
|
|
524
601
|
test('the Agents pane draws live, finished and empty', async ($, on) => {
|
|
525
602
|
const { calls, fn } = recorder()
|
|
526
|
-
drawPane(on, 'agents', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: AGENTS, now: NOW, showDone: false }, { toggleDone: fn('done') }))
|
|
527
|
-
drawPane(on, 'agents-open', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: AGENTS, now: NOW, showDone: true }, { toggleDone: fn('done') }))
|
|
528
|
-
drawPane(on, 'agents-empty', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: [], now: NOW, showDone: false }, { toggleDone: fn('done') }))
|
|
603
|
+
drawPane(on, 'agents', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: AGENTS, now: NOW, showDone: false }, { ...AGENT_NOOP, toggleDone: fn('done') }))
|
|
604
|
+
drawPane(on, 'agents-open', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: AGENTS, now: NOW, showDone: true }, { ...AGENT_NOOP, toggleDone: fn('done') }))
|
|
605
|
+
drawPane(on, 'agents-empty', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: [], now: NOW, showDone: false }, { ...AGENT_NOOP, toggleDone: fn('done') }))
|
|
529
606
|
for (const surface of SURFACES) {
|
|
530
607
|
const ui = await mount($, 'agents', surface, 64)
|
|
531
608
|
expect(await ui.find({ type: 'Text', text: 'Port the request log renderer' }), "ui { type: 'Text', text: 'Port the request log renderer' } " + surface).toBeDefined()
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
expect(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
609
|
+
// What it does now, on the terminal; the desktop's cards hold still and leave it to the
|
|
610
|
+
// conversation and the host's task list (2026-10-20).
|
|
611
|
+
const doing = await ui.find({ type: 'Text', text: /Edit hooks\/ui\/log.tsx · 14s/ })
|
|
612
|
+
if (surface === 'terminal') expect(doing, surface).toBeDefined()
|
|
613
|
+
else expect(doing, surface).toBeUndefined()
|
|
614
|
+
// A finished agent is a card too, not a line behind a fold.
|
|
615
|
+
expect(await ui.find({ type: 'Text', text: 'Summarize the docs' }), surface).toBeDefined()
|
|
616
|
+
// Who picked it and why, on the terminal; the desktop's cards leave it out (2026-10-20).
|
|
617
|
+
const why = await ui.find({ type: 'Text', text: /Frontend \/ Standard/ })
|
|
618
|
+
if (surface === 'terminal') expect(why, surface).toBeDefined()
|
|
619
|
+
else expect(why, surface).toBeUndefined()
|
|
620
|
+
// The API's own counts: 1200 + 38000 + 800 in, 95% of it from the cache, 5100 out, 12 tools.
|
|
621
|
+
if (surface === 'terminal') expect(await ui.find({ type: 'Text', text: 'in 40k · cache 95% · out 5.1k · tools 12' }), surface).toBeDefined()
|
|
622
|
+
else for (const value of ['40k', '95%', '5.1k', '12']) expect(await ui.find({ type: 'Text', text: value }), value).toBeDefined()
|
|
623
|
+
// Two agents finished: nothing hides behind "more".
|
|
624
|
+
expect(await ui.find({ type: 'Button', key: 'done' }), surface).toBeUndefined()
|
|
540
625
|
const empty = await mount($, 'agents-empty', surface, 64)
|
|
541
626
|
expect(await empty.find({ type: 'Text', text: 'No agents yet' }), "empty { type: 'Text', text: 'No agents yet' } " + surface).toBeDefined()
|
|
542
627
|
}
|
|
543
|
-
expect(calls).toEqual([
|
|
628
|
+
expect(calls).toEqual([])
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
test('the Agents pane shows the latest finished agents and the rest behind more; a card opens its conversation', async ($, on) => {
|
|
632
|
+
const { calls, fn } = recorder()
|
|
633
|
+
const many: AgentRec[] = [AGENTS[0]!, ...Array.from({ length: 7 }, (_, i): AgentRec => ({ id: 'd' + i, description: 'Finished task ' + i, type: 'Explore', model: 'claude-haiku-5-5', state: i === 0 ? 'failed' : 'done', startedAt: NOW - 600_000, endedAt: NOW - 500_000 + i * 1000, cost: 0 }))]
|
|
634
|
+
const actions = { ...AGENT_NOOP, toggleDone: fn('done'), open: fn('open') }
|
|
635
|
+
drawPane(on, 'agents-many', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: many, now: NOW, showDone: false }, actions))
|
|
636
|
+
drawPane(on, 'agents-all', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: many, now: NOW, showDone: true }, actions))
|
|
637
|
+
for (const surface of SURFACES) {
|
|
638
|
+
const ui = await mount($, 'agents-many', surface, 64)
|
|
639
|
+
// Newest first: the four latest show, the oldest three wait behind "more".
|
|
640
|
+
for (const i of [6, 5, 4, 3]) expect(await ui.find({ type: 'Text', text: 'Finished task ' + i }), surface + ' ' + i).toBeDefined()
|
|
641
|
+
for (const i of [2, 1, 0]) expect(await ui.find({ type: 'Text', text: 'Finished task ' + i }), surface + ' ' + i).toBeUndefined()
|
|
642
|
+
expect(await ui.find({ type: 'Button', key: 'done' }), surface).toBeDefined()
|
|
643
|
+
await ui.press({ key: 'done' })
|
|
644
|
+
// The desktop presses a card anywhere on it (a blank press a row); the terminal has a button.
|
|
645
|
+
await ui.press({ key: surface === 'terminal' ? 'open-a1' : 'open-a1-0' })
|
|
646
|
+
const all = await mount($, 'agents-all', surface, 64)
|
|
647
|
+
expect(await all.find({ type: 'Text', text: 'Finished task 0' }), surface).toBeDefined()
|
|
648
|
+
expect(await all.find({ type: 'Text', text: 'failed' }) ?? await all.find({ type: 'Text', text: /^✕/ }), surface).toBeDefined()
|
|
649
|
+
if (surface === 'desktop') expect(await all.find({ type: 'Button', key: 'open-d0-2' }), surface).toBeDefined()
|
|
650
|
+
}
|
|
651
|
+
expect(calls).toEqual(['done', 'open:["a1"]', 'done', 'open:["a1"]'])
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
test('an agent\'s conversation reads in the pane: the task, its words, its tools, the answer, and a way back', async ($, on) => {
|
|
655
|
+
const { calls, fn } = recorder()
|
|
656
|
+
const actions = { ...AGENT_NOOP, back: fn('back'), earlier: fn('earlier') }
|
|
657
|
+
const talk: AgentEntry[] = [
|
|
658
|
+
{ kind: 'prompt', text: 'Port the log renderer to the new kit.' },
|
|
659
|
+
{ kind: 'text', text: 'Reading the **current** renderer first.' },
|
|
660
|
+
{ kind: 'tool', id: 't1', tool: 'Read', detail: 'hooks/ui/log.tsx', state: 'ok' },
|
|
661
|
+
{ kind: 'tool', id: 't2', tool: 'Bash', detail: 'npm test', state: 'error' },
|
|
662
|
+
{ kind: 'tool', id: 't3', tool: 'Edit', detail: 'hooks/ui/log.tsx', state: 'running' },
|
|
663
|
+
]
|
|
664
|
+
const done: AgentRec = { ...AGENTS[0]!, id: 'a9', state: 'done', endedAt: NOW - 1000, now: undefined, toolSince: undefined }
|
|
665
|
+
const long: AgentEntry[] = [talk[0]!, ...Array.from({ length: 45 }, (_, i): AgentEntry => ({ kind: 'tool', id: 'x' + i, tool: 'Grep', detail: 'pattern ' + i, state: 'ok' }))]
|
|
666
|
+
drawPane(on, 'talk-live', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: AGENTS, now: NOW, showDone: false, open: { id: 'a1', talk: { entries: talk }, earlier: 0 } }, actions))
|
|
667
|
+
drawPane(on, 'talk-done', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: [done], now: NOW, showDone: false, open: { id: 'a9', talk: { entries: [...talk.slice(0, 3), { kind: 'text', text: 'Ported; tests pass.' }] }, earlier: 0 } }, actions))
|
|
668
|
+
drawPane(on, 'talk-long', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: AGENTS, now: NOW, showDone: false, open: { id: 'a1', talk: { entries: long }, earlier: 0, runsOpen: ['x15'] } }, actions))
|
|
669
|
+
drawPane(on, 'talk-deny', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: AGENTS, now: NOW, showDone: false, open: { id: 'a1', talk: { deny: 'no saved transcript for a1' }, earlier: 0 } }, actions))
|
|
670
|
+
drawPane(on, 'talk-gone', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: AGENTS, now: NOW, showDone: false, open: { id: 'zz', talk: { entries: [] }, earlier: 0 } }, actions))
|
|
671
|
+
for (const surface of SURFACES) {
|
|
672
|
+
const live = await mount($, 'talk-live', surface, 64)
|
|
673
|
+
expect(await live.find({ type: 'Text', text: 'Port the request log renderer' }), surface).toBeDefined()
|
|
674
|
+
expect(await live.find({ type: 'Text', text: 'Task' }), surface).toBeDefined()
|
|
675
|
+
// Its three calls fold into one line; the one it is in now still shows under it.
|
|
676
|
+
expect(await live.find({ type: surface === 'desktop' ? 'Text' : 'Button', text: /^Read 1 file, Ran 1 command, Edited 1 file/ }), surface).toBeDefined()
|
|
677
|
+
expect(await live.find({ type: 'Text', text: '1 failed' }), surface).toBeDefined()
|
|
678
|
+
expect(await live.find({ type: 'Text', text: /^Edit$/ }), surface).toBeDefined()
|
|
679
|
+
for (const tool of ['Read', 'Bash']) expect(await live.find({ type: 'Text', text: new RegExp('^' + tool + '$') }), surface + ' ' + tool).toBeUndefined()
|
|
680
|
+
// Its words as Markdown on the desktop, plain text on the terminal; no answer while it runs.
|
|
681
|
+
if (surface === 'desktop') expect((await live.findAll({ type: 'Markdown' })).some(m => m.props.text === 'Reading the **current** renderer first.'), surface).toBe(true)
|
|
682
|
+
else expect(await live.find({ type: 'Text', text: 'Reading the **current** renderer first.' }), surface).toBeDefined()
|
|
683
|
+
expect(await live.find({ type: 'Text', text: 'Final answer' }), surface).toBeUndefined()
|
|
684
|
+
await live.press({ key: 'back' })
|
|
685
|
+
const finished = await mount($, 'talk-done', surface, 64)
|
|
686
|
+
expect(await finished.find({ type: 'Text', text: 'Final answer' }), surface).toBeDefined()
|
|
687
|
+
const long_ = await mount($, 'talk-long', surface, 64)
|
|
688
|
+
// The task stays at the head; 30 of the 45 calls show, 15 wait behind "earlier".
|
|
689
|
+
expect(await long_.find({ type: 'Text', text: 'pattern 44' }), surface).toBeDefined()
|
|
690
|
+
expect(await long_.find({ type: 'Text', text: 'pattern 14' }), surface).toBeUndefined()
|
|
691
|
+
await long_.press({ key: 'earlier' })
|
|
692
|
+
const deny = await mount($, 'talk-deny', surface, 64)
|
|
693
|
+
expect(await deny.find({ type: 'Text', text: /can’t be read: no saved transcript for a1/ }), surface).toBeDefined()
|
|
694
|
+
const gone = await mount($, 'talk-gone', surface, 64)
|
|
695
|
+
expect(await gone.find({ type: 'Text', text: 'This agent is no longer in the list.' }), surface).toBeDefined()
|
|
696
|
+
await gone.press({ key: 'back' })
|
|
697
|
+
}
|
|
698
|
+
expect(calls).toEqual(['back', 'earlier', 'back', 'back', 'earlier', 'back'])
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
test('talkOf reads a conversation into prompts, words and tool calls that stand answered, failed or running', () => {
|
|
702
|
+
const entries = talkOf([
|
|
703
|
+
{ role: 'user', text: ' Do the thing. ', toolUses: [] },
|
|
704
|
+
{ role: 'assistant', text: 'On it.', toolUses: [
|
|
705
|
+
{ tool_use_id: 'u1', tool: 'Bash', input: { command: 'npm test\nmore' }, text: 'ok' },
|
|
706
|
+
{ tool_use_id: 'u2', tool: 'Read', input: { file_path: '/a.ts' }, text: 'nope', isError: true },
|
|
707
|
+
{ tool_use_id: 'u3', tool: 'Grep', input: { pattern: 'x' } },
|
|
708
|
+
] },
|
|
709
|
+
{ role: 'user', text: '', toolUses: [] },
|
|
710
|
+
])
|
|
711
|
+
expect(entries).toEqual([
|
|
712
|
+
{ kind: 'prompt', text: 'Do the thing.' },
|
|
713
|
+
{ kind: 'text', text: 'On it.' },
|
|
714
|
+
{ kind: 'tool', id: 'u1', tool: 'Bash', detail: 'npm test', state: 'ok' },
|
|
715
|
+
{ kind: 'tool', id: 'u2', tool: 'Read', detail: '/a.ts', state: 'error' },
|
|
716
|
+
{ kind: 'tool', id: 'u3', tool: 'Grep', detail: 'x', state: 'running' },
|
|
717
|
+
])
|
|
544
718
|
})
|
|
545
719
|
|
|
546
720
|
test('the log pane lists this session\'s verdicts among its requests, a fallback in plain words', async ($, on) => {
|
|
@@ -680,8 +854,8 @@ test('no drawing cuts its text: no truncating Text on any surface', async ($, on
|
|
|
680
854
|
const settingsActions = QUICK_ACTIONS
|
|
681
855
|
const cases: [string, Draw][] = [
|
|
682
856
|
['cut-quick', (el, surface) => renderSettings(el, frameOf(surface, 60, 30), { settings: SETTINGS, jev: { source: null, test: { state: 'fail', message: 'key rejected (401) '.repeat(4) } } }, settingsActions)],
|
|
683
|
-
['cut-agents', (el, surface) => renderAgents(el, frameOf(surface, 40), { agents: AGENTS.map(a => ({ ...a, description: a.description + ' and a much longer tail of words that runs past any pane width', state: 'running' as const })), now: NOW, showDone: true }, { toggleDone: noop })],
|
|
684
|
-
['cut-agents-done', (el, surface) => renderAgents(el, frameOf(surface, 40), { agents: AGENTS, now: NOW, showDone: true }, { toggleDone: noop })],
|
|
857
|
+
['cut-agents', (el, surface) => renderAgents(el, frameOf(surface, 40), { agents: AGENTS.map(a => ({ ...a, description: a.description + ' and a much longer tail of words that runs past any pane width', state: 'running' as const })), now: NOW, showDone: true }, { ...AGENT_NOOP, toggleDone: noop })],
|
|
858
|
+
['cut-agents-done', (el, surface) => renderAgents(el, frameOf(surface, 40), { agents: AGENTS, now: NOW, showDone: true }, { ...AGENT_NOOP, toggleDone: noop })],
|
|
685
859
|
['cut-log', (el, surface) => renderLog(el, frameOf(surface, 40), { rows: [{ at: '2026-10-10T13:19:10', source: 'a-very-long-requested-model-name@high', target: 'another-very-long-routed-model-name', provider: 'opencode-go', status: 200, ok: true, ms: 2140 }], mineOnly: false }, { toggleScope: noop })],
|
|
686
860
|
...ALERTS.map((alert, i): [string, Draw] => ['cut-alert-' + i, (el, surface) => renderAlert(el, frameOf(surface, 40), alert, { compact: noop, clear: noop, handoff: noop, dismiss: noop, toggleSave: noop, openSettings: noop, noteInput: noop, setHandoffKind: noop, setHandoffAfter: noop, go: noop, copy: noop, openNew: noop })]),
|
|
687
861
|
]
|
|
@@ -926,12 +1100,13 @@ test('a control\'s tip is drawn in a full band, hidden and revealed by its scope
|
|
|
926
1100
|
const ui = await mount($, 'n100', 'desktop', 100)
|
|
927
1101
|
const tree = await ui.drawn()
|
|
928
1102
|
const tips = tipsOf(tree)
|
|
929
|
-
// One tip per fact and control (the effort, the context, the cache, the limit,
|
|
930
|
-
//
|
|
931
|
-
// and
|
|
932
|
-
expect(tips.length, 'tips: ' + tips.map(t => t.text.slice(0, 14)).join(' / ')).toBe(
|
|
1103
|
+
// One tip per fact and control (the effort, the context, the cache, the limit, and every control
|
|
1104
|
+
// but Handoff, which names itself in its pill) plus the way out — the row is full and all are
|
|
1105
|
+
// drawn. The route has none: it lights as a button, and the log it opens is its card (028aee5).
|
|
1106
|
+
expect(tips.length, 'tips: ' + tips.map(t => t.text.slice(0, 14)).join(' / ')).toBe(11)
|
|
933
1107
|
const words = (start: string) => tips.find(tip => tip.text.startsWith(start))
|
|
934
|
-
|
|
1108
|
+
expect(words('Last route')).toBeUndefined()
|
|
1109
|
+
for (const start of ['Opus 5.5 · Auto', 'Context 38%', 'Prompt cache', 'Weekly limit 42%', 'Effort Auto', 'Keep cache warm', 'Agents:', 'Compact — ', 'New start (/clear) — ', 'Ripple settings', 'Hide the band']) {
|
|
935
1110
|
expect(words(start), start).toBeDefined()
|
|
936
1111
|
}
|
|
937
1112
|
for (const tip of tips) {
|
|
@@ -945,3 +1120,176 @@ test('a control\'s tip is drawn in a full band, hidden and revealed by its scope
|
|
|
945
1120
|
expect(await ui.find({ type: 'Button', key: 'compact' })).toBeDefined()
|
|
946
1121
|
await ui.unmount()
|
|
947
1122
|
})
|
|
1123
|
+
|
|
1124
|
+
test('with the limits row drawn, save mode\'s word follows Claude\'s windows', async ($, on) => {
|
|
1125
|
+
const limits: DashboardView['limits'] = [
|
|
1126
|
+
{ provider: 'claude', account: 0, kind: 'five_hour', label: '', percent: 86 },
|
|
1127
|
+
{ provider: 'claude', account: 0, kind: 'seven_day', label: '', percent: 40 },
|
|
1128
|
+
]
|
|
1129
|
+
const view = (save: boolean): DashboardView => ({ ...DASH, limit: { kind: 'five_hour', percent: 86, save }, limits, extras: ['limits'] })
|
|
1130
|
+
drawPane(on, 'saving', el => renderBand(el, frameOf('desktop', 100), view(true), BAND_ACTIONS))
|
|
1131
|
+
drawPane(on, 'spending', el => renderBand(el, frameOf('desktop', 100), view(false), BAND_ACTIONS))
|
|
1132
|
+
const saving = await mount($, 'saving', 'desktop', 100)
|
|
1133
|
+
expect(await saving.find({ type: 'Text', text: /^save$/ }), 'save word').toBeDefined()
|
|
1134
|
+
// The first row's limit stepped aside: 86% shows once, in the limits row.
|
|
1135
|
+
const at86 = await saving.findAll({ type: 'Text', text: /^86%$/ })
|
|
1136
|
+
expect(at86.length).toBe(1)
|
|
1137
|
+
await saving.unmount()
|
|
1138
|
+
const spending = await mount($, 'spending', 'desktop', 100)
|
|
1139
|
+
expect(await spending.find({ type: 'Text', text: /^save$/ })).toBeUndefined()
|
|
1140
|
+
await spending.unmount()
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
// ---- Tool runs (2026-10-20: a worker's commands read as a wall, each wrapped over lines) ------------
|
|
1145
|
+
|
|
1146
|
+
test('a run of tool calls is headed in one line as the Claude app heads it', () => {
|
|
1147
|
+
const call = (id: string, tool: string, detail: string, state: 'ok' | 'error' | 'running' = 'ok') => ({ id, tool, detail, state })
|
|
1148
|
+
const run = [call('1', 'Bash', 'npm test'), call('2', 'Read', 'a.ts'), call('3', 'Read', 'a.ts'), call('4', 'Bash', 'ls', 'error'), call('5', 'Edit', 'a.ts')]
|
|
1149
|
+
// A file read twice counts once; a read and an edit of one file count in each kind.
|
|
1150
|
+
expect(runSummary(run, 'ko')).toEqual({ text: '명령어 2개 실행함, 파일 1개 읽음, 파일 1개를 편집함', failed: 1 })
|
|
1151
|
+
expect(runSummary(run.slice(0, 2), 'en')).toEqual({ text: 'Ran 1 command, Read 1 file', failed: 0 })
|
|
1152
|
+
expect(runSummary([call('1', 'Bash', 'a'), call('2', 'Bash', 'b')], 'es').text).toBe('2 comandos ejecutados')
|
|
1153
|
+
expect(runSummary([call('1', 'Bash', 'a')], 'es').text).toBe('1 comando ejecutado')
|
|
1154
|
+
// Five kinds or more: the first three, the rest counted.
|
|
1155
|
+
const kinds = [call('1', 'Bash', 'x'), call('2', 'Read', 'a'), call('3', 'Edit', 'b'), call('4', 'Grep', 'c'), call('5', 'WebFetch', 'd'), call('6', 'WebFetch', 'e')]
|
|
1156
|
+
expect(runSummary(kinds, 'ko').text).toBe('명령어 1개 실행함, 파일 1개 읽음, 파일 1개를 편집함, 그 외 작업 3개')
|
|
1157
|
+
expect(runSummary(kinds, 'en').text).toBe('Ran 1 command, Read 1 file, Edited 1 file, and 3 more actions')
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
test('a call reads as one short line: no leading cd, paths short, a command\'s description first', () => {
|
|
1161
|
+
const place = { cwd: '/Users/pbj/Documents/tabripple', home: '/Users/pbj' }
|
|
1162
|
+
expect(commandLine('cd /Users/pbj/Documents/tabripple/android && ./gradlew :core:test -q 2>&1 | grep -vE "^w:"', place)).toBe('./gradlew :core:test -q 2>&1 | grep -vE "^w:"')
|
|
1163
|
+
expect(commandLine('cd "/tmp/a b"; npm test', place)).toBe('npm test')
|
|
1164
|
+
expect(commandLine('cd android\n./gradlew build', place)).toBe('./gradlew build')
|
|
1165
|
+
expect(commandLine('cat > /Users/pbj/Documents/tabripple/notes.md <<\'EOF\'\nline one\nEOF', place)).toBe('cat > notes.md <<\'EOF\'')
|
|
1166
|
+
expect(commandLine('ls /Users/pbj/Documents/tabripple /Users/pbj/.gradle /Users/pbj/Documents/tabripple2', place)).toBe('ls . ~/.gradle ~/Documents/tabripple2')
|
|
1167
|
+
expect(shortPath('/Users/pbj/Documents/tabripple/app/Main.kt', place)).toBe('app/Main.kt')
|
|
1168
|
+
expect(shortPath('/Users/pbj/.zshrc', place)).toBe('~/.zshrc')
|
|
1169
|
+
expect(shortPath('/etc/hosts', place)).toBe('/etc/hosts')
|
|
1170
|
+
expect(shortPath('/Users/pbj/x', { home: '~' })).toBe('/Users/pbj/x')
|
|
1171
|
+
expect(toolDetail('Bash', { command: 'cd /x && npm test', description: 'Run the unit tests\nquietly' }, place)).toBe('Run the unit tests')
|
|
1172
|
+
expect(toolDetail('Bash', { command: 'cd /x && npm test', description: '' }, place)).toBe('npm test')
|
|
1173
|
+
expect(toolDetail('Edit', { file_path: '/Users/pbj/Documents/tabripple/core/A.kt' }, place)).toBe('core/A.kt')
|
|
1174
|
+
expect(toolDetail('Grep', { pattern: 'TODO', path: '/Users/pbj/Documents/tabripple/core' }, place)).toBe('TODO · core')
|
|
1175
|
+
expect(talkOf([{ role: 'assistant', text: '', toolUses: [{ tool_use_id: 'u1', tool: 'Read', input: { file_path: '/Users/pbj/Documents/tabripple/a.kt' }, text: 'ok' }] }], place))
|
|
1176
|
+
.toEqual([{ kind: 'tool', id: 'u1', tool: 'Read', detail: 'a.kt', state: 'ok' }])
|
|
1177
|
+
})
|
|
1178
|
+
|
|
1179
|
+
test('a run of calls folds into its head, unfolds call by call when pressed, and a single call stays one line', async ($, on) => {
|
|
1180
|
+
const { calls, fn } = recorder()
|
|
1181
|
+
const actions = { ...AGENT_NOOP, toggleRun: fn('run') }
|
|
1182
|
+
const bash = (i: number, state: 'ok' | 'error' = 'ok'): AgentEntry => ({ kind: 'tool', id: 'b' + i, tool: 'Bash', detail: commandLine('cd /Users/pbj/Documents/tabripple/android && ./gradlew :core:test --tests "*Parser*" -q 2>&1 | grep -vE "^(w:|warning)" | tail -40 # run ' + i), state })
|
|
1183
|
+
const talk: AgentEntry[] = [
|
|
1184
|
+
{ kind: 'prompt', text: 'Fix the parser tests.' },
|
|
1185
|
+
{ kind: 'text', text: 'Running the suite.' },
|
|
1186
|
+
...Array.from({ length: 8 }, (_, i) => bash(i, i === 3 ? 'error' : 'ok')),
|
|
1187
|
+
{ kind: 'tool', id: 'e1', tool: 'Edit', detail: 'core/src/Parser.kt', state: 'ok' },
|
|
1188
|
+
{ kind: 'text', text: 'One more check.' },
|
|
1189
|
+
{ kind: 'tool', id: 'solo', tool: 'Bash', detail: 'git status', state: 'ok' },
|
|
1190
|
+
]
|
|
1191
|
+
const done: AgentRec = { ...AGENTS[0]!, id: 'a9', state: 'done', endedAt: NOW - 1000, now: undefined, toolSince: undefined }
|
|
1192
|
+
drawPane(on, 'runs-folded', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: [done], now: NOW, showDone: false, open: { id: 'a9', talk: { entries: talk }, earlier: 0 } }, actions))
|
|
1193
|
+
drawPane(on, 'runs-open', (el, surface) => renderAgents(el, frameOf(surface, 64), { agents: [done], now: NOW, showDone: false, open: { id: 'a9', talk: { entries: talk }, earlier: 0, runsOpen: ['b0'] } }, actions))
|
|
1194
|
+
for (const surface of SURFACES) {
|
|
1195
|
+
const folded = await mount($, 'runs-folded', surface, 64)
|
|
1196
|
+
expect(await folded.find({ type: surface === 'desktop' ? 'Text' : 'Button', text: /^Ran 8 commands, Edited 1 file/ }), surface).toBeDefined()
|
|
1197
|
+
expect(await folded.find({ type: 'Text', text: '1 failed' }), surface).toBeDefined()
|
|
1198
|
+
// Folded, none of its calls show; the single call after the words is its own line, no head.
|
|
1199
|
+
expect(await folded.findAll({ type: 'Text', text: /^Bash$/ }), surface).toHaveLength(1)
|
|
1200
|
+
expect(await folded.find({ type: 'Text', text: 'git status' }), surface).toBeDefined()
|
|
1201
|
+
expect(await folded.find({ type: 'Button', key: 'run-solo' }), surface).toBeUndefined()
|
|
1202
|
+
await folded.press({ key: 'run-b0' })
|
|
1203
|
+
const open = await mount($, 'runs-open', surface, 64)
|
|
1204
|
+
expect(await open.findAll({ type: 'Text', text: /^Bash$/ }), surface).toHaveLength(9)
|
|
1205
|
+
expect(await open.find({ type: 'Text', text: /^Edit$/ }), surface).toBeDefined()
|
|
1206
|
+
// Each call one line, its cd gone, cut with an ellipsis before the pane's edge.
|
|
1207
|
+
const details = (await open.findAll({ type: 'Text', text: /gradlew/ })).map(d => d.text)
|
|
1208
|
+
expect(details, surface).toHaveLength(8)
|
|
1209
|
+
for (const detail of details) {
|
|
1210
|
+
expect(detail.startsWith('./gradlew :core:test'), detail).toBe(true)
|
|
1211
|
+
expect(detail.endsWith('…'), detail).toBe(true)
|
|
1212
|
+
// Narrower than the 64-cell pane in the surface's own measure: cells there, the sans's ch here.
|
|
1213
|
+
expect((surface === 'desktop' ? chOf(detail) : displayWidth(detail)) < 64, surface + ' ' + detail).toBe(true)
|
|
1214
|
+
}
|
|
1215
|
+
await open.press({ key: 'run-b0' })
|
|
1216
|
+
}
|
|
1217
|
+
expect(calls).toEqual(['run:["b0"]', 'run:["b0"]', 'run:["b0"]', 'run:["b0"]'])
|
|
1218
|
+
})
|
|
1219
|
+
|
|
1220
|
+
// ---- The cards hold still (2026-10-20: titles re-wrapped as the time ticked, and the pane jumped) --
|
|
1221
|
+
|
|
1222
|
+
const STILL_TITLE = 'Port the request log renderer to the new kit and keep every test green while doing it'
|
|
1223
|
+
const STILL_AGENT: AgentRec = { ...AGENTS[0]!, description: STILL_TITLE, model: 'deepseek-ai/deepseek-v4.1-flash-1017@high', answeredBy: 'deepseek-ai/deepseek-v4.1-flash-1017', provider: 'deepseek' }
|
|
1224
|
+
/** Each pair: the agent's elapsed time before and after one second (a tick, 59s → 1m, 9m 59s → 10m). */
|
|
1225
|
+
const STILL_PAIRS: [number, number][] = [[192_000, 193_000], [59_000, 60_000], [599_000, 600_000]]
|
|
1226
|
+
/** Pane widths: ~560 px, ~360 px, ~260 px. */
|
|
1227
|
+
const STILL_COLUMNS = [68, 44, 33]
|
|
1228
|
+
const STILL_LANGS = ['en', 'ko', 'es'] as const
|
|
1229
|
+
const stillId = (lang: string, columns: number, i: number, side: string) => ['still', lang, columns, i, side].join('-')
|
|
1230
|
+
|
|
1231
|
+
test('a running card draws the same tree a tick on and across 59s → 1m 00s and 9m 59s → 10m 00s, but for its time, wide and narrow', async ($, on) => {
|
|
1232
|
+
for (const lang of STILL_LANGS) {
|
|
1233
|
+
for (const columns of STILL_COLUMNS) {
|
|
1234
|
+
for (const [i, [before, after]] of STILL_PAIRS.entries()) {
|
|
1235
|
+
const agent: AgentRec = { ...STILL_AGENT, startedAt: NOW - before }
|
|
1236
|
+
// A step on, the agent is in another tool: the card must not show it.
|
|
1237
|
+
const later: AgentRec = { ...agent, now: 'Edit hooks/ui/log.tsx', toolSince: NOW + 500 }
|
|
1238
|
+
for (const [side, ms, rec] of [['a', 0, agent], ['b', after - before, later]] as const) {
|
|
1239
|
+
drawPane(on, stillId(lang, columns, i, side), el => renderAgents(el, { ...frameOf('desktop', columns), lang, now: NOW + ms }, { agents: [rec], now: NOW + ms, showDone: false }, AGENT_NOOP))
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
drawPane(on, 'still-fresh', el => renderAgents(el, frameOf('desktop', 68), { agents: [{ ...STILL_AGENT, id: 'a0', usage: undefined, tools: undefined, now: undefined, toolSince: undefined }], now: NOW, showDone: false }, AGENT_NOOP))
|
|
1245
|
+
for (const lang of STILL_LANGS) {
|
|
1246
|
+
for (const columns of STILL_COLUMNS) {
|
|
1247
|
+
for (const [i, [before, after]] of STILL_PAIRS.entries()) {
|
|
1248
|
+
const at = (s: string) => stillId(lang, columns, i, s)
|
|
1249
|
+
const times = [elapsed(before, lang), elapsed(after, lang)]
|
|
1250
|
+
expect(times[0], at('')).not.toBe(times[1])
|
|
1251
|
+
const masked = async (side: string) => {
|
|
1252
|
+
const ui = await mount($, at(side), 'desktop', columns)
|
|
1253
|
+
const tree = unhandled(await ui.drawn()).split(JSON.stringify(times[0])).join('"#"').split(JSON.stringify(times[1])).join('"#"')
|
|
1254
|
+
await ui.unmount()
|
|
1255
|
+
return tree
|
|
1256
|
+
}
|
|
1257
|
+
expect(await masked('b'), at('')).toBe(await masked('a'))
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// The numbers rows are there from the start, a dash for each figure not yet reported.
|
|
1262
|
+
const fresh = await mount($, 'still-fresh', 'desktop', 68)
|
|
1263
|
+
expect(await fresh.findAll({ type: 'Text', text: /^–$/ })).toHaveLength(3)
|
|
1264
|
+
expect(await fresh.find({ type: 'Text', text: /^0$/ })).toBeDefined()
|
|
1265
|
+
for (const lang of ['en', 'ko', 'zh', 'ja', 'es', 'pt'] as const) {
|
|
1266
|
+
for (const columns of STILL_COLUMNS) {
|
|
1267
|
+
const frame = { ...frameOf('desktop', columns), lang }
|
|
1268
|
+
// The time's column holds its widest value in every language.
|
|
1269
|
+
for (const ms of [59_000, 3_599_000, 99 * 3_600_000 + 59 * 60_000]) expect(chOf(elapsed(ms, lang)) <= timeCells(frame, STILL_AGENT), lang + ' ' + elapsed(ms, lang)).toBe(true)
|
|
1270
|
+
// Growing figures never add a row.
|
|
1271
|
+
expect(cardRows(frame, STILL_AGENT), lang + ' ' + columns).toBe(cardRows(frame, { ...STILL_AGENT, usage: { input: 9_900_000, cached: 90_000_000, cacheWrite: 0, output: 99_000_000 }, tools: 999 }))
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
})
|
|
1275
|
+
|
|
1276
|
+
test('a narrow card keeps its model on one line without the vendor path, and its numbers in two fixed rows', async ($, on) => {
|
|
1277
|
+
const why = 'session model · General / Standard'
|
|
1278
|
+
const agent: AgentRec = { ...STILL_AGENT, pick: { ...STILL_AGENT.pick!, why, by: 'session' } }
|
|
1279
|
+
drawPane(on, 'narrow-card', el => renderAgents(el, { ...frameOf('desktop', 33), lang: 'ko' }, { agents: [agent], now: NOW, showDone: false }, AGENT_NOOP))
|
|
1280
|
+
drawPane(on, 'wide-card', el => renderAgents(el, { ...frameOf('desktop', 68), lang: 'ko' }, { agents: [agent], now: NOW, showDone: false }, AGENT_NOOP))
|
|
1281
|
+
const narrow = await mount($, 'narrow-card', 'desktop', 33)
|
|
1282
|
+
const wide = await mount($, 'wide-card', 'desktop', 68)
|
|
1283
|
+
for (const ui of [narrow, wide]) {
|
|
1284
|
+
// No vendor path, and no pick line.
|
|
1285
|
+
expect(await ui.find({ type: 'Text', text: /deepseek-ai\// })).toBeUndefined()
|
|
1286
|
+
expect(await ui.find({ type: 'Text', text: /^deepseek-v/ })).toBeDefined()
|
|
1287
|
+
expect(await ui.find({ type: 'Text', text: /표준/ })).toBeUndefined()
|
|
1288
|
+
expect(await ui.find({ type: 'Text', text: why })).toBeUndefined()
|
|
1289
|
+
}
|
|
1290
|
+
// Narrow: the name is cut to fit beside the effort; the numbers take two rows, the time ends one.
|
|
1291
|
+
expect((await narrow.find({ type: 'Text', text: /^deepseek-v/ }))!.text.endsWith('…')).toBe(true)
|
|
1292
|
+
expect(await narrow.find({ key: 'nums-1' })).toBeDefined()
|
|
1293
|
+
expect(await wide.find({ key: 'nums-1' })).toBeUndefined()
|
|
1294
|
+
expect(await narrow.find({ type: 'Text', text: '3m 12s' }) ?? await narrow.find({ type: 'Text', text: '3분 12초' })).toBeDefined()
|
|
1295
|
+
})
|