@ucsandman/legcli 0.7.0 → 0.8.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 +12 -0
- package/README.md +16 -14
- package/bin/leg.mjs +47 -26
- package/docs/adapters.md +17 -3
- package/docs/cli-contracts.md +36 -3
- package/docs/configuration.md +20 -1
- package/docs/getting-started.md +3 -3
- package/fixtures/limits/grok/grok-rate-limit.json +11 -0
- package/fixtures/live/agy/limit-agy-resource-exhausted.json +11 -0
- package/package.json +3 -3
- package/scripts/build-docs-site.mjs +4 -3
- package/src/accounts.mjs +10 -1
- package/src/adapters/grok.mjs +4 -7
- package/src/attach.mjs +87 -18
- package/src/board/board.css +45 -17
- package/src/board/board.js +1 -1
- package/src/board/floor.js +2 -2
- package/src/board/sessions.js +104 -35
- package/src/land.mjs +688 -47
- package/src/launcher.mjs +2 -1
- package/src/mergequeue.mjs +1 -1
- package/src/preferences.mjs +28 -8
- package/src/runner.mjs +1 -1
- package/src/server.mjs +32 -11
- package/src/sessions.mjs +3 -2
- package/src/taps/grok.mjs +251 -0
package/src/attach.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
15
15
|
import { sanitizeEnv } from './env.mjs'
|
|
16
16
|
import { home } from './store.mjs'
|
|
17
17
|
import { get as getAdapter } from './adapters/index.mjs'
|
|
18
|
-
import {
|
|
18
|
+
import { SUPERVISED_AGENTS, HANDOFF_ORDER_CAPABILITY, newSessionId, createSession, readSession, updateSession, appendEvent, takeControl, sessionDir, listSessions, reapLost, isActive, workRoot } from './sessions.mjs'
|
|
19
19
|
import { ensure as ensureWorktree, remove as removeWorktree } from './worktree.mjs'
|
|
20
20
|
import { canonPath, realPath } from './fsx.mjs'
|
|
21
21
|
import { whoami, readShare, isOn as shareIsOn } from './share.mjs'
|
|
@@ -26,6 +26,7 @@ import { writeSettings, userStatusLine, transcriptTail as claudeTail } from './t
|
|
|
26
26
|
import { ensureTrust, trustLine } from './trust.mjs'
|
|
27
27
|
import { findRollout, createTail, parseLines, readCodexUsage, transcriptTail as codexTail } from './taps/codex.mjs'
|
|
28
28
|
import { scanLog, promptsSince, logSize } from './taps/agy.mjs'
|
|
29
|
+
import { fetchGrokUsage, scanLog as scanGrokLog, promptsSince as grokPromptsSince } from './taps/grok.mjs'
|
|
29
30
|
import { fetchClaudeUsage } from './taps/claude-usage.mjs'
|
|
30
31
|
import { saveSessionBundle, resumePrompt } from './bundle.mjs'
|
|
31
32
|
import { endSessionPointer } from './resume.mjs'
|
|
@@ -33,7 +34,7 @@ import { openBoard, pidfile } from './launcher.mjs'
|
|
|
33
34
|
import { LAYOUT } from './accounts.mjs'
|
|
34
35
|
import { captureLive } from './live-capture.mjs'
|
|
35
36
|
import { waitForReset, fmtCountdown } from './wait.mjs'
|
|
36
|
-
import { readPreferences, normalizeHandoffOrder } from './preferences.mjs'
|
|
37
|
+
import { readPreferences, normalizeHandoffOrder, resolveAutoApprove } from './preferences.mjs'
|
|
37
38
|
|
|
38
39
|
const SRC = dirname(fileURLToPath(import.meta.url))
|
|
39
40
|
const SERVER = join(SRC, 'server.mjs')
|
|
@@ -56,7 +57,7 @@ export function isCurrentLeg(session, { pid, agent, account }) {
|
|
|
56
57
|
// ---- board ----
|
|
57
58
|
function health(port, host = '127.0.0.1') {
|
|
58
59
|
return new Promise((res) => {
|
|
59
|
-
const req = http.get({ host, port, path: '/api/health', timeout:
|
|
60
|
+
const req = http.get({ host, port, path: '/api/health', timeout: 4000 }, (r) => {
|
|
60
61
|
let d = ''
|
|
61
62
|
r.on('data', (c) => { d += c })
|
|
62
63
|
r.on('end', () => {
|
|
@@ -84,11 +85,12 @@ export async function ensureBoard({ open = true } = {}) {
|
|
|
84
85
|
child.unref()
|
|
85
86
|
const t0 = Date.now()
|
|
86
87
|
while (Date.now() - t0 < 15000) {
|
|
87
|
-
|
|
88
|
+
const h = await health(port, host)
|
|
89
|
+
if (h) {
|
|
88
90
|
// only claim the pidfile for a child we actually started: under a race,
|
|
89
91
|
// another `baton` won the port and ours died on EADDRINUSE — writing our
|
|
90
92
|
// dead pid would make `baton down` kill nothing and report "not running"
|
|
91
|
-
const ours = child.exitCode === null && Boolean(child.pid)
|
|
93
|
+
const ours = h.pid ? h.pid === child.pid : (child.exitCode === null && Boolean(child.pid))
|
|
92
94
|
if (ours) writeFileSync(pidfile(), JSON.stringify({ pid: child.pid, port, bind: host, children: [child.pid], detached: true, started_by: 'attach', started_at: new Date().toISOString() }, null, 2) + '\n')
|
|
93
95
|
if (open) openBoard(url)
|
|
94
96
|
return { url, started: ours }
|
|
@@ -138,13 +140,18 @@ export function isolate({ g, cwd, sid, sessions = reapLost(listSessions()) }) {
|
|
|
138
140
|
// a binary that is not installed (that spawned ENOENT and killed the session
|
|
139
141
|
// with exit 127 instead of waiting for a reset). Resolves each adapter the way
|
|
140
142
|
// the runner would spawn it (native exe, npm entry, or BATON_<AGENT>_BIN).
|
|
143
|
+
async function loadAdapter(name) {
|
|
144
|
+
if (name === 'grok') return (await import('./adapters/grok.mjs')).default
|
|
145
|
+
return getAdapter(name)
|
|
146
|
+
}
|
|
147
|
+
|
|
141
148
|
let installedCache = null
|
|
142
149
|
async function installedAgents() {
|
|
143
150
|
if (installedCache) return installedCache
|
|
144
151
|
const out = {}
|
|
145
|
-
for (const name of
|
|
152
|
+
for (const name of SUPERVISED_AGENTS) {
|
|
146
153
|
try {
|
|
147
|
-
const { bin, viaNode, entry } = (await
|
|
154
|
+
const { bin, viaNode, entry } = (await loadAdapter(name)).resolve()
|
|
148
155
|
const target = viaNode ? (entry ?? bin) : bin
|
|
149
156
|
if (/[\\/]/.test(target)) out[name] = existsSync(target)
|
|
150
157
|
else { const r = spawnSync(target, ['--version'], { windowsHide: true, encoding: 'utf8', timeout: 8000 }); out[name] = !r.error && r.status === 0 }
|
|
@@ -182,26 +189,36 @@ function restoreTerminal() {
|
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
// ---- spawn spec per agent ----
|
|
185
|
-
export async function spawnSpec(agent, { account, args, sessionId, prompt, cwd }) {
|
|
186
|
-
const adapter = await
|
|
192
|
+
export async function spawnSpec(agent, { account, args, sessionId, prompt, cwd, autoApprove = resolveAutoApprove() }) {
|
|
193
|
+
const adapter = await loadAdapter(agent)
|
|
187
194
|
const { bin, viaNode, entry } = adapter.resolve()
|
|
188
195
|
const argv = []
|
|
189
196
|
// viaNode: either an npm entry (codex bin/codex.js) or a BATON_<AGENT>_BIN that names a .mjs (tests)
|
|
190
197
|
if (viaNode) argv.push(entry ?? bin)
|
|
191
198
|
// a leg Baton starts on its own (after a hand-off) takes BATON_<AGENT>_ARGS,
|
|
192
199
|
// e.g. BATON_CODEX_ARGS="-m gpt-5-mini" to keep a test chain on cheap models
|
|
193
|
-
if (prompt) args = [...(process.env[`BATON_${agent.toUpperCase()}_ARGS`] ?? '').split(/\s+/).filter(Boolean), ...args]
|
|
200
|
+
if (prompt) args = [...(process.env[`LEG_${agent.toUpperCase()}_ARGS`] ?? process.env[`BATON_${agent.toUpperCase()}_ARGS`] ?? '').split(/\s+/).filter(Boolean), ...args]
|
|
194
201
|
if (agent === 'claude') {
|
|
195
202
|
const settings = writeSettings(sessionId, { statusLine: userStatusLine(process.env.CLAUDE_CONFIG_DIR || (account !== 'default' ? envFor('claude', account).CLAUDE_CONFIG_DIR : undefined)) })
|
|
196
|
-
|
|
203
|
+
const autoFlags = autoApprove && !args.includes('--dangerously-skip-permissions') ? ['--dangerously-skip-permissions'] : [] // auto-approve: not forbidden for interactive sessions
|
|
204
|
+
argv.push(...args, ...autoFlags, '--settings', settings)
|
|
197
205
|
if (prompt) argv.push(prompt)
|
|
198
206
|
} else if (agent === 'codex') {
|
|
199
|
-
|
|
207
|
+
const hasApproval = args.includes('--ask-for-approval') || args.includes('-a') || args.some((x) => typeof x === 'string' && x.startsWith('--ask-for-approval='))
|
|
208
|
+
const autoFlags = autoApprove && !hasApproval ? ['--ask-for-approval', 'never'] : []
|
|
209
|
+
argv.push(...args, ...autoFlags)
|
|
200
210
|
if (prompt) argv.push(prompt)
|
|
201
211
|
} else if (agent === 'agy') {
|
|
202
212
|
const log = join(sessionDir(sessionId), 'agy.log')
|
|
203
|
-
|
|
213
|
+
const autoFlags = autoApprove && !args.includes('--dangerously-skip-permissions') ? ['--dangerously-skip-permissions'] : [] // auto-approve: not forbidden for interactive sessions
|
|
214
|
+
argv.push(...args, ...autoFlags, '--log-file', log)
|
|
204
215
|
if (prompt) argv.push('-i', prompt)
|
|
216
|
+
} else if (agent === 'grok') {
|
|
217
|
+
const log = join(sessionDir(sessionId), 'grok.log')
|
|
218
|
+
const hasApprove = args.includes('--always-approve') || args.includes('--yolo') || args.includes('--approval-mode=yolo') // auto-approve check: not forbidden for interactive sessions
|
|
219
|
+
const autoFlags = autoApprove && !hasApprove ? ['--always-approve'] : [] // auto-approve: not forbidden for interactive sessions
|
|
220
|
+
argv.push(...args, ...autoFlags, '--debug-file', log)
|
|
221
|
+
if (prompt) argv.push(prompt)
|
|
205
222
|
}
|
|
206
223
|
const env = { ...sanitizeEnv(process.env, { interactive: true }), ...envFor(agent, account), LEG_SESSION: sessionId, BATON_SESSION: sessionId }
|
|
207
224
|
return { bin: viaNode ? process.execPath : bin, args: argv, env, cwd }
|
|
@@ -209,7 +226,7 @@ export async function spawnSpec(agent, { account, args, sessionId, prompt, cwd }
|
|
|
209
226
|
|
|
210
227
|
// ---- one agent leg ----
|
|
211
228
|
// Returns { reason: 'exit'|'limit'|'handoff', code }
|
|
212
|
-
async function runLeg({ agent, account, args, session, prompt, boardUrl }) {
|
|
229
|
+
async function runLeg({ agent, account, args, session, prompt, boardUrl, autoApprove = resolveAutoApprove() }) {
|
|
213
230
|
const sid = session.session_id
|
|
214
231
|
refreshAccount(agent, account)
|
|
215
232
|
// A handoff happens when the limit hits, which is usually when nobody is
|
|
@@ -219,7 +236,7 @@ async function runLeg({ agent, account, args, session, prompt, boardUrl }) {
|
|
|
219
236
|
const trust = ensureTrust(agent, session.cwd, { cwd: session.cwd })
|
|
220
237
|
const trusted = trustLine(trust)
|
|
221
238
|
if (trusted) { say(trusted); appendEvent(sid, { type: 'trust', summary: trusted }) }
|
|
222
|
-
const spec = await spawnSpec(agent, { account, args, sessionId: sid, prompt, cwd: session.cwd })
|
|
239
|
+
const spec = await spawnSpec(agent, { account, args, sessionId: sid, prompt, cwd: session.cwd, autoApprove })
|
|
223
240
|
appendEvent(sid, { type: 'leg', summary: `${agent} (${account}) starting${prompt ? ' from the handoff bundle' : ''}` })
|
|
224
241
|
const startedMs = Date.now()
|
|
225
242
|
const turnsAtLegStart = session.turns ?? 0
|
|
@@ -227,6 +244,8 @@ async function runLeg({ agent, account, args, session, prompt, boardUrl }) {
|
|
|
227
244
|
// the end of what the first one wrote, or it walls itself on that leg's line
|
|
228
245
|
const agyLog = agent === 'agy' ? join(sessionDir(sid), 'agy.log') : null
|
|
229
246
|
const agyTail = agyLog ? createTail(agyLog, { from: logSize(agyLog) }) : null
|
|
247
|
+
const grokLog = agent === 'grok' ? join(sessionDir(sid), 'grok.log') : null
|
|
248
|
+
const grokTail = grokLog ? createTail(grokLog, { from: logSize(grokLog) }) : null
|
|
230
249
|
let child
|
|
231
250
|
try {
|
|
232
251
|
child = spawn(spec.bin, spec.args, { cwd: spec.cwd, env: spec.env, stdio: 'inherit', windowsHide: false })
|
|
@@ -289,6 +308,24 @@ async function runLeg({ agent, account, args, session, prompt, boardUrl }) {
|
|
|
289
308
|
pollUsage().catch(() => {})
|
|
290
309
|
usageTimer = setInterval(() => pollUsage().catch(() => {}), USAGE_MS)
|
|
291
310
|
usageTimer.unref?.()
|
|
311
|
+
} else if (agent === 'grok') {
|
|
312
|
+
const pollUsage = async () => {
|
|
313
|
+
const configDir = spec.env.GROK_HOME || LAYOUT.grok.home()
|
|
314
|
+
const r = await fetchGrokUsage({ configDir })
|
|
315
|
+
const s = readSession(sid)
|
|
316
|
+
if (!isCurrentLeg(s, { pid: child.pid, agent, account })) return
|
|
317
|
+
const usable = r.ok && r.limits && (r.limits.five_hour || r.limits.seven_day)
|
|
318
|
+
if (usable) {
|
|
319
|
+
recordUsage('grok', account, r.limits, 'grok billing proxy')
|
|
320
|
+
updateSession(sid, { limits: r.limits, usage_source: 'grok billing proxy', usage_error: null })
|
|
321
|
+
} else if (!s.usage_error) {
|
|
322
|
+
const why = r.error ?? 'the usage endpoint answered with no window'
|
|
323
|
+
updateSession(sid, { usage_error: why }, { event: { type: 'status', summary: `grok usage unavailable: ${why}` } })
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
pollUsage().catch(() => {})
|
|
327
|
+
usageTimer = setInterval(() => pollUsage().catch(() => {}), USAGE_MS)
|
|
328
|
+
usageTimer.unref?.()
|
|
292
329
|
}
|
|
293
330
|
|
|
294
331
|
const timer = setInterval(() => {
|
|
@@ -351,6 +388,25 @@ async function runLeg({ agent, account, args, session, prompt, boardUrl }) {
|
|
|
351
388
|
if (s.status === 'starting') patch.status = 'running'
|
|
352
389
|
}
|
|
353
390
|
}
|
|
391
|
+
// grok: log text + prompts
|
|
392
|
+
if (agent === 'grok') {
|
|
393
|
+
const text = grokTail ? grokTail.read().join('\n') : ''
|
|
394
|
+
const hit = text ? scanGrokLog(text) : null
|
|
395
|
+
if (hit && s.status !== 'limit') {
|
|
396
|
+
const u = markLimited('grok', account, { resets_at: hit.resets_at, reason: hit.signal, source: 'grok log' })
|
|
397
|
+
patch.status = 'limit'; patch.limit = { reason: hit.signal, detail: hit.detail, resets_at: hit.resets_at ?? u.limited_until, at: new Date().toISOString() }
|
|
398
|
+
appendEvent(sid, { type: 'limit', summary: `grok limit (${hit.signal}): ${hit.detail.slice(0, 160)}` })
|
|
399
|
+
try { captureLive('grok', hit.signal, { log_excerpt: hit.detail, resets_at: hit.resets_at }, { sessionId: sid }) } catch {}
|
|
400
|
+
}
|
|
401
|
+
const prompts = grokPromptsSince({ grokHome: spec.env.GROK_HOME || LAYOUT.grok.home(), cwd: s.cwd, sinceMs: startedMs })
|
|
402
|
+
const turns = turnsAtLegStart + prompts.length
|
|
403
|
+
if (prompts.length && turns !== (s.turns ?? 0)) {
|
|
404
|
+
patch.turns = turns; patch.last_activity = new Date().toISOString()
|
|
405
|
+
if (!s.task) patch.task = prompts[0].text.slice(0, 500)
|
|
406
|
+
if (!s.agent_session_id && prompts[0].sessionId) patch.agent_session_id = prompts[0].sessionId
|
|
407
|
+
if (s.status === 'starting') patch.status = 'running'
|
|
408
|
+
}
|
|
409
|
+
}
|
|
354
410
|
// warning threshold (every agent; claude's limits arrive via the usage poller)
|
|
355
411
|
{
|
|
356
412
|
const lim = patch.limits ?? s.limits
|
|
@@ -456,7 +512,7 @@ export function claimHandoffChoice({ sid, agent, account, installed, bundle = nu
|
|
|
456
512
|
|
|
457
513
|
// ---- the command ----
|
|
458
514
|
export async function attach(agent, args = [], { open = true } = {}) {
|
|
459
|
-
if (!
|
|
515
|
+
if (!SUPERVISED_AGENTS.includes(agent)) throw new Error(`unknown agent "${agent}" (claude|codex|agy|grok)`)
|
|
460
516
|
// the paid gate: a valid key, or no session (exit 4). The bare agent is never
|
|
461
517
|
// affected; only what Baton adds is licensed.
|
|
462
518
|
const ent = entitlement()
|
|
@@ -464,13 +520,22 @@ export async function attach(agent, args = [], { open = true } = {}) {
|
|
|
464
520
|
// --no-worktree is Baton's flag, not the agent's: it never passes through
|
|
465
521
|
const shareCheckout = args.includes('--no-worktree')
|
|
466
522
|
args = args.filter((a) => a !== '--no-worktree')
|
|
523
|
+
let autoApproveCli = null
|
|
524
|
+
if (args.includes('--no-auto-approve')) {
|
|
525
|
+
autoApproveCli = false
|
|
526
|
+
args = args.filter((a) => a !== '--no-auto-approve')
|
|
527
|
+
} else if (args.includes('--auto-approve')) {
|
|
528
|
+
autoApproveCli = true
|
|
529
|
+
args = args.filter((a) => a !== '--auto-approve')
|
|
530
|
+
}
|
|
531
|
+
const autoApprove = resolveAutoApprove({ cliFlag: autoApproveCli })
|
|
467
532
|
const cwd = process.cwd()
|
|
468
533
|
const board = await ensureBoard({ open })
|
|
469
534
|
let accounts = readAccounts()
|
|
470
535
|
const installed = await installedAgents()
|
|
471
536
|
const handoffOrder = readPreferences().handoff_order
|
|
472
537
|
let account = process.env.LEG_ACCOUNT || process.env.BATON_ACCOUNT || 'default'
|
|
473
|
-
if (!accounts[agent].includes(account)) { say(`no ${agent} account "${account}"; using default`); account = 'default' }
|
|
538
|
+
if (!(accounts[agent] ?? ['default']).includes(account)) { say(`no ${agent} account "${account}"; using default`); account = 'default' }
|
|
474
539
|
// A persisted wall is only a cache. Ask Codex's read-only account endpoint
|
|
475
540
|
// before using it to skip this login; an explicit true can clear an older
|
|
476
541
|
// wall, false refreshes it, and unknown preserves it.
|
|
@@ -478,6 +543,10 @@ export async function attach(agent, args = [], { open = true } = {}) {
|
|
|
478
543
|
const codexHome = envFor('codex', account).CODEX_HOME || LAYOUT.codex.home()
|
|
479
544
|
await refreshCodexUsage(account, codexHome).catch(() => {})
|
|
480
545
|
}
|
|
546
|
+
if (agent === 'grok' && installed.grok && !process.env.BATON_GROK_BIN) {
|
|
547
|
+
const grokHome = envFor('grok', account).GROK_HOME || LAYOUT.grok.home()
|
|
548
|
+
await fetchGrokUsage({ configDir: grokHome }).catch(() => {})
|
|
549
|
+
}
|
|
481
550
|
// Start on an account that is not at its wall, if we already know one is.
|
|
482
551
|
const nowS = Math.floor(Date.now() / 1000)
|
|
483
552
|
const u0 = readUsage(agent, account)
|
|
@@ -513,7 +582,7 @@ export async function attach(agent, args = [], { open = true } = {}) {
|
|
|
513
582
|
// bounds a wait; a normal session runs one leg and exits
|
|
514
583
|
for (let leg = 0; ; leg++) {
|
|
515
584
|
const s = readSession(sid)
|
|
516
|
-
const r = await runLeg({ agent, account, args: legArgs, session: s, prompt, boardUrl: board.url })
|
|
585
|
+
const r = await runLeg({ agent, account, args: legArgs, session: s, prompt, boardUrl: board.url, autoApprove })
|
|
517
586
|
if (r.reason === 'exit') { exit = r.code ?? 0; break }
|
|
518
587
|
// limit or handoff: bundle, choose next, go again in this terminal
|
|
519
588
|
const cur = readSession(sid)
|
package/src/board/board.css
CHANGED
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
--id-claude: #FCA169; /* oklch(0.790 0.130 52) warm */
|
|
130
130
|
--id-codex: #69DBBA; /* oklch(0.815 0.115 172) cool green */
|
|
131
131
|
--id-agy: #CC97F3; /* oklch(0.760 0.140 310) violet, 46 degrees clear of the accent */
|
|
132
|
+
--id-grok: #70B8FF; /* oklch(0.750 0.130 240) azure blue */
|
|
132
133
|
--id-fake: #A0A6AE; /* oklch(0.720 0.010 258) the scripted adapter, neutral in hue */
|
|
133
134
|
|
|
134
135
|
/* type scale. Fixed rem, range 13 to 52. The old board ran 13 to 21, which
|
|
@@ -188,10 +189,10 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
188
189
|
|
|
189
190
|
.banner { background: var(--e1); border-bottom: 1px solid var(--edge); color: var(--text-2); font-size: var(--t-0); padding: 10px clamp(20px, 4vw, 40px); padding-top: calc(10px + env(safe-area-inset-top, 0px)); }
|
|
190
191
|
|
|
191
|
-
.masthead { display: flex; align-items:
|
|
192
|
+
.masthead { position: relative; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding-block: 28px 24px; }
|
|
192
193
|
.brand { font-size: var(--t-1); font-weight: var(--w-head); letter-spacing: -.01em; }
|
|
193
194
|
.brand-glyph { display: inline-block; margin-right: 4px; }
|
|
194
|
-
.masthead-right { display: flex; align-items:
|
|
195
|
+
.masthead-right { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
|
|
195
196
|
.floor-link { color: var(--text-2); text-decoration: none; border-bottom: 1px solid var(--line-strong); padding-bottom: 1px; }
|
|
196
197
|
.floor-link:hover { color: var(--text); border-bottom-color: var(--text-2); }
|
|
197
198
|
|
|
@@ -220,6 +221,7 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
220
221
|
.dot.id-claude { background: var(--id-claude); }
|
|
221
222
|
.dot.id-codex { background: var(--id-codex); }
|
|
222
223
|
.dot.id-agy { background: var(--id-agy); }
|
|
224
|
+
.dot.id-grok { background: var(--id-grok); }
|
|
223
225
|
|
|
224
226
|
/* ---- logins ------------------------------------------------------------- */
|
|
225
227
|
.logins { display: grid; gap: 20px; }
|
|
@@ -304,6 +306,14 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
304
306
|
.file.is-overlap { color: var(--warn-text); }
|
|
305
307
|
.blocker { margin-top: 10px; font-size: var(--t-0); color: var(--text-3); max-width: 64ch; }
|
|
306
308
|
.blocker.is-hoisted { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
309
|
+
.why-cant-land { margin-top: 10px; font-size: var(--t-0); color: var(--text-3); max-width: 64ch; }
|
|
310
|
+
.why-cant-land-summary { cursor: pointer; color: var(--text-2); font-weight: var(--w-head); user-select: none; }
|
|
311
|
+
.why-cant-land-summary:hover { color: var(--text-1); }
|
|
312
|
+
.blocker-list { list-style: none; margin: 8px 0 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
313
|
+
.blocker-item { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 10px; background: var(--surface-2); border-radius: var(--r-control); font-size: var(--t--1); }
|
|
314
|
+
.blocker-msg { color: var(--text-2); flex: 1 1 auto; }
|
|
315
|
+
.blocker-fixes { display: inline-flex; gap: 6px; }
|
|
316
|
+
.btn-sm { min-height: 26px; padding: 0 10px; font-size: var(--t--1); }
|
|
307
317
|
|
|
308
318
|
/* the flat inline data token. Not a capsule: a chip here is a word set in the
|
|
309
319
|
meta colour beside the thing it qualifies */
|
|
@@ -312,6 +322,7 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
312
322
|
.chip-id-claude { color: var(--id-claude); }
|
|
313
323
|
.chip-id-codex { color: var(--id-codex); }
|
|
314
324
|
.chip-id-agy { color: var(--id-agy); }
|
|
325
|
+
.chip-id-grok { color: var(--id-grok); }
|
|
315
326
|
.chip-id-fake { color: var(--id-fake); }
|
|
316
327
|
.chip-state-ok { color: var(--ok-text); }
|
|
317
328
|
.chip-state-warn { color: var(--warn-text); }
|
|
@@ -320,6 +331,7 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
320
331
|
.acct-name.id-claude { color: var(--id-claude); }
|
|
321
332
|
.acct-name.id-codex { color: var(--id-codex); }
|
|
322
333
|
.acct-name.id-agy { color: var(--id-agy); }
|
|
334
|
+
.acct-name.id-grok { color: var(--id-grok); }
|
|
323
335
|
.acct-name.id-fake { color: var(--id-fake); }
|
|
324
336
|
|
|
325
337
|
/* ---- controls ----------------------------------------------------------- */
|
|
@@ -345,14 +357,17 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
345
357
|
/* the four scroll boxes board-drawer.test.mjs scans for by line prefix. Each
|
|
346
358
|
class is the first token on its line, in this order, and no earlier line in
|
|
347
359
|
this file begins with any of the four */
|
|
348
|
-
.drawer-task { max-height: 22rem; overflow: auto; font-size: var(--t-0); color: var(--text-
|
|
349
|
-
.drawer-msg p { max-height: 18rem; overflow-y: auto; font-size: var(--t-0); color: var(--text
|
|
350
|
-
.drawer-timeline { max-height: 22rem; overflow: auto; font-size: var(--t-0); }
|
|
351
|
-
.drawer-diff { max-height: 26rem; overflow: auto; font-family: var(--mono); font-size: var(--t--1); }
|
|
352
|
-
|
|
353
|
-
.drawer-msg { margin-top: 14px; }
|
|
354
|
-
.drawer-msg-
|
|
355
|
-
.drawer-msg-
|
|
360
|
+
.drawer-task { max-height: 22rem; overflow: auto; font-size: var(--t-0); color: var(--text); background: var(--e0); border: 1px solid var(--edge); border-radius: var(--r-control); padding: 14px 18px; line-height: 1.55; }
|
|
361
|
+
.drawer-msg p { max-height: 18rem; overflow-y: auto; font-size: var(--t-0); color: var(--text); line-height: 1.55; margin: 0; }
|
|
362
|
+
.drawer-timeline { max-height: 22rem; overflow: auto; font-size: var(--t-0); background: var(--e0); border: 1px solid var(--edge); border-radius: var(--r-control); padding: 8px 16px; }
|
|
363
|
+
.drawer-diff { max-height: 26rem; overflow: auto; font-family: var(--mono); font-size: var(--t--1); background: var(--e0); border-top: 1px solid var(--line); margin-top: 10px; padding-top: 10px; }
|
|
364
|
+
|
|
365
|
+
.drawer-msg { margin-top: 12px; background: var(--e0); border: 1px solid var(--edge); border-radius: var(--r-control); padding: 14px 18px; }
|
|
366
|
+
.drawer-msg.is-human { border-left: 3px solid var(--accent); }
|
|
367
|
+
.drawer-msg.is-agent { border-left: 3px solid var(--ok-text); }
|
|
368
|
+
.drawer-msg-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
369
|
+
.drawer-msg-role { font-size: var(--t--1); color: var(--text-2); font-weight: var(--w-head); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
370
|
+
.drawer-msg-when { font-size: var(--t--1); color: var(--text-3); font-family: var(--mono); }
|
|
356
371
|
|
|
357
372
|
/* ---- ledger: on the ground, unpanelled --------------------------------- */
|
|
358
373
|
.ledger { margin-top: 56px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 40px; padding-top: 32px; border-top: 1px solid var(--edge); }
|
|
@@ -418,20 +433,32 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
418
433
|
.worktree { color: var(--text-3); }
|
|
419
434
|
|
|
420
435
|
/* ---- detail panes ------------------------------------------------------- */
|
|
421
|
-
.detail { margin-top: 22px; background: var(--e1); border-radius: var(--r-well); box-shadow: var(--sink); }
|
|
436
|
+
.detail { margin-top: 22px; background: var(--e1); border-radius: var(--r-well); box-shadow: var(--sink); border: 1px solid var(--edge); }
|
|
422
437
|
.detail-inner { padding: 24px; }
|
|
423
|
-
.detail-
|
|
424
|
-
.detail-
|
|
425
|
-
.detail-
|
|
438
|
+
.detail-masthead { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding-bottom: 18px; border-bottom: 1px solid var(--line); margin-bottom: 22px; }
|
|
439
|
+
.detail-brand { display: flex; align-items: center; gap: 10px; font-size: var(--t-1); }
|
|
440
|
+
.detail-ctrls { display: flex; align-items: center; gap: 10px; }
|
|
441
|
+
.detail-heading { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 12px; font-size: var(--t-1); font-weight: var(--w-head); margin-bottom: 14px; letter-spacing: -.01em; }
|
|
442
|
+
.detail-section { margin-top: 26px; padding-top: 22px; border-top: 1px solid var(--line); }
|
|
443
|
+
.detail-section:first-of-type { margin-top: 0; padding-top: 0; border-top: 0; }
|
|
444
|
+
.detail-sub { font-size: var(--t-0); color: var(--text-3); font-weight: var(--w-text); }
|
|
426
445
|
.kv { display: grid; grid-template-columns: 148px 1fr; gap: 10px 20px; font-size: var(--t-0); }
|
|
427
446
|
.kv-key { color: var(--text-3); }
|
|
428
447
|
.kv-val { color: var(--text); }
|
|
429
|
-
.
|
|
448
|
+
.detail .kv { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
|
|
449
|
+
.well { background: var(--e0); border-radius: var(--r-control); padding: 14px 18px; border: 1px solid var(--edge); }
|
|
430
450
|
.log-pre { font-family: var(--mono); font-size: var(--t--1); white-space: pre-wrap; color: var(--text-2); max-height: 26rem; overflow: auto; }
|
|
431
451
|
.hunk { font-family: var(--mono); font-size: var(--t--1); white-space: pre-wrap; }
|
|
432
|
-
.file-
|
|
452
|
+
.drawer-file-item { background: var(--e0); border: 1px solid var(--edge); border-radius: var(--r-control); padding: 10px 14px; margin-bottom: 8px; }
|
|
453
|
+
.drawer-file-item:hover { border-color: var(--line-strong); }
|
|
454
|
+
.file-row { display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 10px; text-align: left; }
|
|
433
455
|
.turn { padding: 12px 0; }
|
|
434
456
|
.turn + .turn { border-top: 1px solid var(--line); }
|
|
457
|
+
.timeline-item { padding: 10px 0; display: grid; grid-template-columns: 80px 140px 1fr; gap: 12px; align-items: baseline; }
|
|
458
|
+
.timeline-item + .timeline-item { border-top: 1px solid var(--line); }
|
|
459
|
+
.timeline-item .turn-when { color: var(--text-3); font-size: var(--t--1); }
|
|
460
|
+
.timeline-item .turn-role { color: var(--accent); font-size: var(--t--1); font-weight: var(--w-head); }
|
|
461
|
+
.timeline-summary { color: var(--text-2); margin: 0; font-size: var(--t-0); }
|
|
435
462
|
.turn-role { font-size: var(--t--1); color: var(--text-3); font-weight: var(--w-head); }
|
|
436
463
|
.turn-when { font-size: var(--t--1); color: var(--text-3); }
|
|
437
464
|
/* the kind word sits beside the clock, not welded to it: `10:40 AMstarted` */
|
|
@@ -448,9 +475,10 @@ code { font-family: var(--mono); font-size: 0.94em; }
|
|
|
448
475
|
.actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
449
476
|
|
|
450
477
|
/* ---- the floor: the scheduler's view, tables on the same ground --------- */
|
|
451
|
-
.masthead-left { display: flex; align-items:
|
|
478
|
+
.masthead-left { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
|
|
452
479
|
.repos-status, .counts-status { color: var(--text-3); }
|
|
453
480
|
.counts-status span { color: var(--text-2); font-weight: var(--w-head); margin-left: 2px; }
|
|
481
|
+
#floor-accounts { margin-top: 16px; }
|
|
454
482
|
.region-floor { margin-top: 12px; }
|
|
455
483
|
.ftable { width: 100%; border-collapse: collapse; background: var(--e2); border: 1px solid var(--edge); border-radius: var(--r-panel); box-shadow: var(--lift); overflow: hidden; font-size: var(--t-0); }
|
|
456
484
|
.ftable th, .ftable td { text-align: left; vertical-align: top; padding: 14px 18px; }
|
package/src/board/board.js
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
'read-only': 'read only',
|
|
57
57
|
plan: 'plan only',
|
|
58
58
|
}
|
|
59
|
-
const AGENT_IDS = ['claude', 'codex', 'agy']
|
|
59
|
+
const AGENT_IDS = ['claude', 'codex', 'agy', 'grok']
|
|
60
60
|
const DEFAULT_BIND = '127.0.0.1:4747'
|
|
61
61
|
const TIMELINE_CAP = 12
|
|
62
62
|
// mirrors LOOPBACK in src/auth.mjs; state.bind is "<host>:<port>" and an IPv6
|
package/src/board/floor.js
CHANGED
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
// the same commit.
|
|
131
131
|
|
|
132
132
|
const WIN_WORDS = { '5h': '5 hour', '7d': '7 day' }
|
|
133
|
-
const IDS = ['claude', 'codex', 'agy', 'fake']
|
|
133
|
+
const IDS = ['claude', 'codex', 'agy', 'grok', 'fake']
|
|
134
134
|
|
|
135
135
|
// ---- times. The head prints `Times are local.` once, so no row repeats it ----
|
|
136
136
|
// THIS FILE OWNS THE TIME GRAMMAR FOR THE WHOLE BOARD. ago(), clockAt(),
|
|
@@ -600,7 +600,7 @@
|
|
|
600
600
|
if (s === 'reconnecting') {
|
|
601
601
|
banner.textContent = state.lastReadingAt ? `Reconnecting to Leg. Last reading ${clockAt(state.lastReadingAt.getTime())}.` : `Reconnecting to Leg on ${state.bind}.`
|
|
602
602
|
} else if (s === 'connecting') {
|
|
603
|
-
banner.textContent = `Connecting to
|
|
603
|
+
banner.textContent = `Connecting to Leg on ${state.bind}.`
|
|
604
604
|
}
|
|
605
605
|
}
|
|
606
606
|
|
package/src/board/sessions.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
handing_off: ['handing off', 'warn'], waiting: ['waiting for reset', 'warn'], handed_off: ['handed off', 'idle'], ended: ['ended', 'idle'], lost: ['lost', 'danger'],
|
|
23
23
|
}
|
|
24
24
|
const WIN_WORDS = { '5h': '5 hour', '7d': '7 day' }
|
|
25
|
-
const IDS = ['claude', 'codex', 'agy', 'fake']
|
|
25
|
+
const IDS = ['claude', 'codex', 'agy', 'grok', 'fake']
|
|
26
26
|
|
|
27
27
|
let view = null
|
|
28
28
|
const NO_BRANCH_BLOCKER = 'this terminal works in the checkout itself: there is no branch of its own to land'
|
|
@@ -643,15 +643,15 @@
|
|
|
643
643
|
// A result that belongs to a terminal is written into that terminal's sentence
|
|
644
644
|
// slot, where the reader is already looking. Only a result that belongs to no
|
|
645
645
|
// object on this page goes to the one system message.
|
|
646
|
-
async function act(id, action, btn) {
|
|
646
|
+
async function act(id, action, btn, body = null) {
|
|
647
647
|
btn.disabled = true
|
|
648
648
|
actionNotes.delete(id)
|
|
649
649
|
try {
|
|
650
650
|
if (action.startsWith('requests/')) {
|
|
651
|
-
await api(`/api/sessions/${encodeURIComponent(id)}/${action}`, { method: 'POST' })
|
|
651
|
+
await api(`/api/sessions/${encodeURIComponent(id)}/${action}`, { method: 'POST', body })
|
|
652
652
|
actionNotes.set(id, { at: Date.now(), tone: 'ok', text: action.endsWith('approve') ? 'approved; this terminal hands off in a few seconds' : 'the request was dismissed' })
|
|
653
653
|
} else if (action === 'request-handoff') {
|
|
654
|
-
await api(`/api/sessions/${encodeURIComponent(id)}/request-handoff`, { method: 'POST' })
|
|
654
|
+
await api(`/api/sessions/${encodeURIComponent(id)}/request-handoff`, { method: 'POST', body })
|
|
655
655
|
sysMessage('asked; the owner of that terminal decides', 'ok')
|
|
656
656
|
} else if (action === 'remove') {
|
|
657
657
|
const r = await api(`/api/sessions/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
|
@@ -660,9 +660,10 @@
|
|
|
660
660
|
await api(`/api/sessions/${encodeURIComponent(id)}?force=1&keep_worktree=1`, { method: 'DELETE' })
|
|
661
661
|
sysMessage('removed the Leg record; the worktree and the branch are kept', 'ok')
|
|
662
662
|
} else {
|
|
663
|
-
await api(`/api/sessions/${encodeURIComponent(id)}/${action}`, { method: 'POST' })
|
|
663
|
+
await api(`/api/sessions/${encodeURIComponent(id)}/${action}`, { method: 'POST', body })
|
|
664
664
|
if (action === 'handoff') actionNotes.set(id, { at: Date.now(), tone: 'warn', text: 'hand-off requested; this terminal switches agents in a few seconds' })
|
|
665
665
|
else if (action === 'end') actionNotes.set(id, { at: Date.now(), tone: 'warn', text: 'end requested; the agent stops after its current turn' })
|
|
666
|
+
else if (action === 'land/fix') actionNotes.set(id, { at: Date.now(), tone: 'ok', text: 'applied fix' })
|
|
666
667
|
}
|
|
667
668
|
refresh()
|
|
668
669
|
} catch (err) {
|
|
@@ -849,25 +850,63 @@
|
|
|
849
850
|
// G10: the order is Land, Hand off now, Details, End, and it never reflows
|
|
850
851
|
// by availability. A button that does not apply is omitted, never moved.
|
|
851
852
|
const landing = Boolean(s.land && s.land.state === 'landing')
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
// needs it, and nothing connected the sentence below to the button above.
|
|
853
|
+
const cl = s.can_land || (s.worktree ? (s.land_blocker ? { ok: false, blockers: [{ code: 'legacy', message: s.land_blocker }] } : { ok: true, blockers: [] }) : { ok: false, blockers: [{ code: 'no_worktree', message: NO_BRANCH_BLOCKER }] })
|
|
854
|
+
const isOnTarget = cl.blockers && cl.blockers.some((b) => b.code === 'on_target_branch')
|
|
855
|
+
const targetLocked = cl.blockers && cl.blockers.find((b) => b.code === 'target_locked')
|
|
856
|
+
const blocker = !cl.ok ? (cl.blockers[0]?.message || NO_BRANCH_BLOCKER) : null
|
|
857
857
|
const blockerId = blocker ? `land-blocker-${s.session_id}` : null
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
'
|
|
866
|
-
'
|
|
867
|
-
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
|
|
858
|
+
|
|
859
|
+
if (isOnTarget) {
|
|
860
|
+
const commitDirect = el('button', {
|
|
861
|
+
type: 'button',
|
|
862
|
+
class: 'btn btn-secondary',
|
|
863
|
+
'data-focus-key': `commit-direct:${s.session_id}`,
|
|
864
|
+
title: `Commit directly on ${s.branch || s.worktree?.base || 'main'}`,
|
|
865
|
+
}, ['Commit directly'])
|
|
866
|
+
commitDirect.addEventListener('click', () => act(s.session_id, 'land/fix', commitDirect, { action: 'commit_directly' }))
|
|
867
|
+
actions.appendChild(commitDirect)
|
|
868
|
+
} else {
|
|
869
|
+
const landLabel = landing ? 'Landing…' : targetLocked ? targetLocked.message : 'Land'
|
|
870
|
+
const landDisabled = !cl.ok || landing
|
|
871
|
+
const land = el('button', {
|
|
872
|
+
type: 'button',
|
|
873
|
+
class: `btn ${landDisabled ? 'btn-secondary' : 'btn-primary'}${landing ? ' is-loading' : ''}`,
|
|
874
|
+
disabled: landDisabled ? '' : null,
|
|
875
|
+
'data-focus-key': `land:${s.session_id}`,
|
|
876
|
+
'aria-describedby': blockerId,
|
|
877
|
+
title: blocker || (s.worktree ? `commit work on ${s.worktree.branch}, rebase onto ${s.worktree.base}, test, and fast-forward ${s.worktree.base}` : 'Land'),
|
|
878
|
+
}, [landLabel])
|
|
879
|
+
if (!landDisabled) {
|
|
880
|
+
land.addEventListener('click', async () => {
|
|
881
|
+
land.disabled = true
|
|
882
|
+
land.classList.add('is-loading')
|
|
883
|
+
land.textContent = 'Preparing…'
|
|
884
|
+
try {
|
|
885
|
+
const res = await api(`/api/sessions/${encodeURIComponent(s.session_id)}/land/prepare`, { method: 'POST' })
|
|
886
|
+
land.classList.remove('is-loading')
|
|
887
|
+
if (!res.ok) {
|
|
888
|
+
actionNotes.set(s.session_id, { at: Date.now(), tone: 'danger', text: res.error || 'Prepare failed' })
|
|
889
|
+
refresh()
|
|
890
|
+
return
|
|
891
|
+
}
|
|
892
|
+
const statText = res.diff_stat || `${res.files?.length || 0} files changed`
|
|
893
|
+
const question = `Prepared: ${statText} · tests green. Land onto ${s.worktree?.base || 'main'} and ship to GitHub?`
|
|
894
|
+
pendingConfirm = {
|
|
895
|
+
id: s.session_id,
|
|
896
|
+
question,
|
|
897
|
+
verb: 'Land',
|
|
898
|
+
action: 'land',
|
|
899
|
+
}
|
|
900
|
+
renderSessions(view)
|
|
901
|
+
} catch (err) {
|
|
902
|
+
land.classList.remove('is-loading')
|
|
903
|
+
actionNotes.set(s.session_id, { at: Date.now(), tone: 'danger', text: err.message })
|
|
904
|
+
refresh()
|
|
905
|
+
}
|
|
906
|
+
})
|
|
907
|
+
}
|
|
908
|
+
actions.appendChild(land)
|
|
909
|
+
}
|
|
871
910
|
if (s.active) {
|
|
872
911
|
const h = el('button', { type: 'button', class: `btn ${blocker ? 'btn-primary' : 'btn-secondary'}`, title: 'save the bundle, stop this agent, start the next option in the same terminal', 'data-focus-key': `handoff:${s.session_id}` }, ['Hand off now'])
|
|
873
912
|
h.addEventListener('click', () => act(s.session_id, 'handoff', h))
|
|
@@ -901,8 +940,33 @@
|
|
|
901
940
|
}
|
|
902
941
|
row.appendChild(actions)
|
|
903
942
|
term.appendChild(row)
|
|
904
|
-
|
|
905
|
-
|
|
943
|
+
|
|
944
|
+
// Why can't I land expander or fallback blocker message
|
|
945
|
+
if (!cl.ok && !isOnTarget && s.worktree) {
|
|
946
|
+
const list = el('ul', { class: 'blocker-list' })
|
|
947
|
+
for (const b of cl.blockers) {
|
|
948
|
+
const item = el('li', { class: 'blocker-item' })
|
|
949
|
+
item.appendChild(el('span', { class: 'blocker-msg' }, [b.message]))
|
|
950
|
+
const fixes = b.fixes || (b.fix ? [b.fix] : [])
|
|
951
|
+
if (fixes.length) {
|
|
952
|
+
const grp = el('span', { class: 'blocker-fixes' })
|
|
953
|
+
for (const f of fixes) {
|
|
954
|
+
const fBtn = el('button', { type: 'button', class: 'btn btn-sm btn-secondary', title: f.label }, [f.label])
|
|
955
|
+
fBtn.addEventListener('click', () => act(s.session_id, 'land/fix', fBtn, { action: f.action, target_session: f.target_session }))
|
|
956
|
+
grp.appendChild(fBtn)
|
|
957
|
+
}
|
|
958
|
+
item.appendChild(grp)
|
|
959
|
+
}
|
|
960
|
+
list.appendChild(item)
|
|
961
|
+
}
|
|
962
|
+
const expander = el('details', { class: 'why-cant-land', id: blockerId }, [
|
|
963
|
+
el('summary', { class: 'why-cant-land-summary' }, ["Why can't I land?"]),
|
|
964
|
+
list,
|
|
965
|
+
])
|
|
966
|
+
term.appendChild(expander)
|
|
967
|
+
} else if (blocker) {
|
|
968
|
+
term.appendChild(el('p', { class: 'blocker', id: blockerId }, [blocker]))
|
|
969
|
+
}
|
|
906
970
|
return term
|
|
907
971
|
}
|
|
908
972
|
|
|
@@ -1014,9 +1078,12 @@
|
|
|
1014
1078
|
}
|
|
1015
1079
|
|
|
1016
1080
|
function messageRow(m, key) {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1081
|
+
const isUser = m.role === 'user'
|
|
1082
|
+
return el('div', { class: `turn drawer-msg ${isUser ? 'is-human' : 'is-agent'}` }, [
|
|
1083
|
+
el('div', { class: 'drawer-msg-head' }, [
|
|
1084
|
+
el('span', { class: `turn-role drawer-msg-role ${isUser ? '' : 'chip-state-ok'}` }, [isUser ? 'human' : 'agent']),
|
|
1085
|
+
m.ts ? el('span', { class: 'turn-when drawer-msg-when' }, [whenAgo(m.ts)]) : null,
|
|
1086
|
+
]),
|
|
1020
1087
|
// every box that can scroll carries a key, so where the reader had
|
|
1021
1088
|
// scrolled to survives the rebuild three seconds later
|
|
1022
1089
|
el('p', { 'data-scroll-key': `msg:${key}` }, [m.text]),
|
|
@@ -1024,7 +1091,7 @@
|
|
|
1024
1091
|
}
|
|
1025
1092
|
|
|
1026
1093
|
function fileRow(f) {
|
|
1027
|
-
const wrap = el('div', {})
|
|
1094
|
+
const wrap = el('div', { class: 'drawer-file-item' })
|
|
1028
1095
|
const pre = el('pre', { class: 'drawer-diff', hidden: '', 'data-scroll-key': `diff:${f.path}` })
|
|
1029
1096
|
const row = el('button', { type: 'button', class: 'btn btn-text file-row', 'aria-expanded': 'false', 'data-focus-key': `file:${f.path}` }, [
|
|
1030
1097
|
el('span', { class: 'mono', title: f.path }, [f.path]),
|
|
@@ -1115,16 +1182,18 @@
|
|
|
1115
1182
|
return
|
|
1116
1183
|
}
|
|
1117
1184
|
const [label] = STATUS[s.status] || [s.status]
|
|
1118
|
-
const
|
|
1185
|
+
const left = el('div', { class: 'detail-brand' }, [
|
|
1186
|
+
el('span', { class: `dot id-${idOf(s.agent)}` }),
|
|
1119
1187
|
el('span', { class: `acct-name chip-id-${idOf(s.agent)}` }, [s.agent]),
|
|
1120
|
-
el('span', { class: 'chip' }, [tail(s.session_id)]),
|
|
1188
|
+
el('span', { class: 'chip mono' }, [tail(s.session_id)]),
|
|
1189
|
+
el('span', { class: s.active ? 'chip chip-state-ok' : 'chip is-stale' }, [s.active ? (label || 'running') : (label || s.status)]),
|
|
1121
1190
|
])
|
|
1122
1191
|
const pause = el('button', { type: 'button', class: 'btn btn-secondary', id: 'session-drawer-pause', 'data-focus-key': 'drawer-pause' }, [drawer.paused ? 'Resume updates' : 'Pause updates'])
|
|
1123
1192
|
pause.addEventListener('click', () => { drawer.paused = !drawer.paused; if (!drawer.paused) loadDrawer(); else renderDrawer() })
|
|
1124
1193
|
const close = el('button', { type: 'button', class: 'btn btn-secondary', id: 'session-drawer-close', 'data-focus-key': 'drawer-close' }, ['Close'])
|
|
1125
1194
|
close.addEventListener('click', closeSessionDrawer)
|
|
1126
|
-
|
|
1127
|
-
box.appendChild(
|
|
1195
|
+
const header = el('div', { class: 'detail-masthead' }, [left, el('div', { class: 'detail-ctrls' }, [pause, close])])
|
|
1196
|
+
box.appendChild(header)
|
|
1128
1197
|
if (drawer.error) box.appendChild(el('p', { class: 'sentence tone-danger' }, [drawer.error]))
|
|
1129
1198
|
|
|
1130
1199
|
const last = d && d.messages ? [...d.messages].reverse().find((m) => m.role === 'assistant') : null
|
|
@@ -1171,10 +1240,10 @@
|
|
|
1171
1240
|
// header on the same panel printing the same instant as 11:04 PM. The
|
|
1172
1241
|
// summary is a block, as board.js:754 builds the same row, or the kind
|
|
1173
1242
|
// word and the sentence render glued: `lostrunner pid 999002 is gone`.
|
|
1174
|
-
timeline.appendChild(el('div', { class: 'turn' }, [
|
|
1243
|
+
timeline.appendChild(el('div', { class: 'turn timeline-item' }, [
|
|
1175
1244
|
el('span', { class: 'mono turn-when' }, [clockAt(Date.parse(e.ts))]),
|
|
1176
1245
|
el('span', { class: 'turn-role' }, [e.type]),
|
|
1177
|
-
el('p', {}, [e.summary || '']),
|
|
1246
|
+
el('p', { class: 'timeline-summary' }, [e.summary || '']),
|
|
1178
1247
|
]))
|
|
1179
1248
|
}
|
|
1180
1249
|
if (!events.length) timeline.appendChild(el('p', { class: 'sentence tone-muted' }, [d ? 'nothing recorded yet' : 'reading the timeline']))
|