@leftoff/cli 0.9.17-beta.9 → 0.9.18-beta.1
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 +34 -0
- package/bin/leftoff.js +541 -487
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +150 -2
- package/ui/app.js +384 -30
- package/ui/ask.js +1 -0
- package/ui/back-line.js +5 -2
- package/ui/diff-view.js +71 -10
- package/ui/icons.js +4 -0
- package/ui/live-view.js +3 -3
- package/ui/recap-line.js +5 -2
- package/ui/semdiff.js +284 -56
- package/ui/session-catchup.js +5 -1
- package/ui/start.js +87 -12
- package/ui/walk-lines.js +119 -0
- package/ui/walk-md.js +40 -0
- package/ui/walk-pop.js +24 -5
- package/ui/walk-spots.js +89 -0
package/ui/app.js
CHANGED
|
@@ -23,7 +23,7 @@ import { parseBang, dropBang, ptyInput, commandOutput, outputMessage, createBang
|
|
|
23
23
|
import { withFresh, pickFresh, newerFresh, freshWins, pendingHeadless, settled as headlessSettled } from './headless-fresh.js' // 0.9.9 another Mac's session, current after Stop/Resume/Permissions
|
|
24
24
|
import { bgPillHtml } from './bgtasks.js' // 0.9.13 "2 tasks running"
|
|
25
25
|
import { scopeHtml, scopeHintHtml, scopeStart } from './sm-scope.js' // 0.9.15 per-machine summarizer
|
|
26
|
-
import { startPageHtml, startPick, startAsksBanner, headlessBarHtml, hmHelpId, modePillHtml, modeLabel, startsSettingHtml, localPillHtml, lockedAttrs, lockHtml, startViewerLocal as startViewerAt, startTexts, fillStartTexts, browseTarget, startPairAskHtml, typedPath, fillApproved, startBody, startExtrasNote, startProjectLineHtml, START_SID, startAttachTodo, newDraftId } from './start.js' // 0.9.9 New session
|
|
26
|
+
import { startPageHtml, startPick, startAsksBanner, headlessBarHtml, hmHelpId, modePillHtml, termPillHtml, termBarHtml, modeLabel, startsSettingHtml, localPillHtml, lockedAttrs, lockHtml, startViewerLocal as startViewerAt, startTexts, fillStartTexts, browseTarget, startPairAskHtml, typedPath, fillApproved, startBody, startExtrasNote, startProjectLineHtml, START_SID, startAttachTodo, newDraftId } from './start.js' // 0.9.9 New session
|
|
27
27
|
import { parseNewSession, projectStartItems, newSessionContext, startStateFor } from './cmdk-new.js' // 0.9.13 ⌘K New session…
|
|
28
28
|
import { attachPlan, skippedText, shortName, prepareFile, uploadFile, fetchAttachment, fmtSize, attachKind, pastedName, ATTACH_MAX_FILES } from './attach.js' // 0.9.8 attachments
|
|
29
29
|
import { wireTips } from './tips.js'
|
|
@@ -54,7 +54,9 @@ import { TABS_KEY, layoutFor, parseStored, serialize, openTab, promote, remember
|
|
|
54
54
|
import { fsSideHtml, createSideStore, railHtml, createWidthStore, clampSideWidth, sideMax, SIDE_W_STEP, SIDE_W_DEFAULT, WIDE_MIN } from './fs-side.js'
|
|
55
55
|
import { fileReach, gateFileRefs } from './file-reach.js' // 2026-10-29 a path links only when its file can be read
|
|
56
56
|
import { createFoldStore, foldHtml, relPath, rootsOf, filesTouchedHtml, dropJunkCps, statusStripHtml, answeredHtml, whereWords } from './side-fold.js' // 2026-10-29 a calmer session panel
|
|
57
|
-
import { walkthroughHtml, hydrateWalkthrough, fmToggle, eyeCountHtml, reviewedSet, stepAfter, visibleSteps, stepPos, toMarkdown, toHtmlDoc, exportName } from './semdiff.js' // 0.9.17 Walk me through it
|
|
57
|
+
import { walkthroughHtml, hydrateWalkthrough, fmToggle, eyeCountHtml, reviewedSet, stepAfter, visibleSteps, stepPos, toMarkdown, toHtmlDoc, exportName, revNextState, revNextLabel, REVNEXT_TIP, REVFINISH_TIP, sfState, sfSet, patchFor, readingOrder, SPLIT_MIN, RAIL_MIN as WT_SIDE_MIN, RAIL_MAX as WT_SIDE_MAX, RAIL_DEFAULT as WT_SIDE_DEFAULT } from './semdiff.js' // 0.9.17 Walk me through it
|
|
58
|
+
import { rowsOf, pickLine, dragTo, describePicks, refsPrompt } from './walk-lines.js' // 2026-10-30 line refs for Ask
|
|
59
|
+
import { spotsFor, nextSpotIndex, spotCountWords } from './walk-spots.js' // 2026-10-30 Next spot
|
|
58
60
|
import { walkButtonHtml, sizeLineHtml, popContent, initPops } from './walk-pop.js' // 0.9.18 Walk me through it's size line and popover
|
|
59
61
|
import { lastReply, ghostShows } from './ghost.js'
|
|
60
62
|
import { digestHeadline, todayHidden, hideToday, todayCardHtml } from './today.js' // 0.9.16 the board's Today card // 0.9.16 the suggested next message // 0.9.16 the session panel beside full screen
|
|
@@ -66,7 +68,7 @@ import { copyButtonHtml, copyClick } from './copy-msg.js' // 0.9.11: Copy on Cla
|
|
|
66
68
|
import { codeCopyClick } from './code-copy.js' // 0.9.15: Copy on code blocks
|
|
67
69
|
import { refBody, refErrorBody, refBarHtml, sizeLabel as refSize } from './file-view.js' // 0.9.15: open a file from a message
|
|
68
70
|
import { refAgainst } from './file-links.js'
|
|
69
|
-
import { hydrateDiffs, renderDiff } from './diff-view.js' // 0.9.8: a file's changes drawn like GitHub's
|
|
71
|
+
import { hydrateDiffs, renderDiff, renderSplit } from './diff-view.js' // 0.9.8: a file's changes drawn like GitHub's
|
|
70
72
|
import { loginBannersHtml, loginSig, dismissedKeys as loginDismissed, dismissLogin, offerDismissed as loginOfferOff, dismissOffer as loginDismissOffer } from './login-banner.js' // 0.9.14 Claude login expiry
|
|
71
73
|
import { isDone, stillWorking, canMarkDone, DONE_CONFIRM, DONE_TIP, UNDONE_TIP, doneChipHtml } from './done.js' // 0.9.15 Done
|
|
72
74
|
import { mountTerminal, escInTerminal } from './term.js' // 0.9.15 the embedded terminal
|
|
@@ -250,6 +252,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
250
252
|
if (!isNavKey(e, { typing, term, mac: navMac }) || !navRailQ.matches) return
|
|
251
253
|
if (document.querySelector('.lo-menu, .cmdk:not([hidden]), .lightbox:not([hidden]), .pdfview:not([hidden])')) return
|
|
252
254
|
if ([...document.querySelectorAll('[role="dialog"][aria-modal="true"]')].some((d) => d.id !== 'navDrawer' && !d.closest('[hidden]') && d.getClientRects().length)) return
|
|
255
|
+
if (e.key === '[' && wtListUp()) return // 2026-10-30 in a walkthrough with a file list, [ folds that list (the walkthrough's own keys)
|
|
253
256
|
e.preventDefault()
|
|
254
257
|
toggleNav()
|
|
255
258
|
}, true)
|
|
@@ -284,7 +287,7 @@ const $app = document.getElementById('app')
|
|
|
284
287
|
const scrollKeep = createScrollKeep()
|
|
285
288
|
scrollKeep.start()
|
|
286
289
|
// 0.9.17 Walk me through it: which session shows it, and its view (this page only; the open one survives a reload)
|
|
287
|
-
const wtUi = { sid: null, scope: new Map(), view: 'story', filter: 'all', data: new Map(), loading: new Set(), error: new Map(), cur: new Map(), code: new Set(), groups: new Set(), pending: [], ask: '', node: null, sig: '', restored: false, fellBack: new Set() }
|
|
290
|
+
const wtUi = { sid: null, scope: new Map(), view: 'story', filter: 'all', data: new Map(), loading: new Set(), error: new Map(), cur: new Map(), code: new Set(), groups: new Set(), pending: [], ask: '', node: null, sig: '', restored: false, fellBack: new Set(), picks: [], picksFp: '', spot: new Map(), checking: new Set() } // 2026-10-30 picks: lines picked for Ask (ui/walk-lines.js); spot: fp -> where Next spot is
|
|
288
291
|
// 0.9.8: the pulsing dots (Live while Claude works, active, a blocking ask) keep their beat through the board's redraws
|
|
289
292
|
if (typeof MutationObserver === 'function') new MutationObserver(() => lockPulses(document)).observe(document.body, { childList: true, subtree: true })
|
|
290
293
|
const state = { sessions: [], health: null, cfg: null, route: null, machine: localStorage.getItem('cp.machine') || '', drafts: {}, pendingAsk: {}, mayAnswer: true, needOpen: {}, needTurns: {} }
|
|
@@ -526,7 +529,7 @@ function agentChip(a, { title } = {}) {
|
|
|
526
529
|
|
|
527
530
|
// 0.8.0: a Codex session wears a small "Codex" badge; Claude Code's wear none.
|
|
528
531
|
function codexBadge(s) {
|
|
529
|
-
return s && s.codingAgent === 'codex' ? '<span class="chip agent-badge codex"
|
|
532
|
+
return s && s.codingAgent === 'codex' ? '<span class="chip agent-badge codex" data-tip="' + esc('OpenAI Codex session' + (s.surface === 'codex-ide' ? ' (IDE)' : s.surface === 'codex-app' ? ' (app)' : '')) + '">Codex</span>' : ''
|
|
530
533
|
}
|
|
531
534
|
|
|
532
535
|
// 0.7.1: a session in a git worktree says which ("⑂ fix-login"); one filed
|
|
@@ -1085,7 +1088,7 @@ function card(s) {
|
|
|
1085
1088
|
'<div class="row1"><div class="title">' + cardGlyph(s) +
|
|
1086
1089
|
(s.pinned ? '<span class="pin">★ </span>' : '') + '<a href="#/s/' + encodeURIComponent(s.id) + '">' + titleHtml(sessionTitle(s), esc) + '</a>' +
|
|
1087
1090
|
'<button type="button" class="icon" data-action="rename" title="Rename">✎</button>' +
|
|
1088
|
-
'</div>' + statusPill(s) + liveBadge(s) + eyeCountHtml(s.lookCount, { esc }) + '</div>' +
|
|
1091
|
+
'</div>' + statusPill(s) + liveBadge(s) + termPillHtml(s, { esc }) + eyeCountHtml(s.lookCount, { esc }) + '</div>' +
|
|
1089
1092
|
'<div class="meta">' + spaceChip(s) + where(s, { repo: !titleIsWhere(s) }) + (ln && !off ? '<span class="livenote ' + esc(s.live) + '">' + esc(ln) + '</span>' : '') + (endedNote ? '<span>' + esc(endedNote) + '</span>' : '') + '<span>·</span><span>' + esc(s.turns) + (s.turns === 1 ? ' turn' : ' turns') + (s.historyTurns ? ' · resumed (' + esc(s.historyTurns) + ' before)' : s.startSource === 'resume' && !s.turns ? ' · resumed' : s.empty && s.live !== 'closed' && !cardPills(s).live ? ' · no prompt yet' : '') + '</span>' +
|
|
1090
1093
|
(s.needsInput && s.lastNotification && !s.needsYou && !off ? '<span class="chip">' + esc(s.lastNotification.replace(/_/g, ' ')) + '</span>' : '') + '</div>' +
|
|
1091
1094
|
cardPrsHtml(s.prs) + listChips(s) + cardHandoffChips(s) + nb + recapLineHtml(s, { esc, ago, clock }) + cardCheckpoint(s) + beforeClearHtml(s, { esc }) +
|
|
@@ -1120,8 +1123,26 @@ async function wtOpen(sid, scope = null) {
|
|
|
1120
1123
|
if (!isFullLive(sid)) { location.hash = fullHref(sid); return }
|
|
1121
1124
|
if (phoneNow() && fsSheetUi.open === sid) fsSheetUi.open = null
|
|
1122
1125
|
repaintSession(sid)
|
|
1126
|
+
const had = wtUi.data.get(wtKey(sid))
|
|
1127
|
+
if (had && had.status === 'ok' && !wtUi.loading.has(wtKey(sid))) wtCheck(sid) // 2026-10-30 back to one it has: shown at once, checked quietly
|
|
1123
1128
|
wtLoad(sid)
|
|
1124
1129
|
}
|
|
1130
|
+
// 2026-10-30 (asked: "is it stored in a way that we can nav away and nav back to it?") coming back to a
|
|
1131
|
+
// walkthrough this page already has shows it at once (no model run, no spinner) and asks the daemon,
|
|
1132
|
+
// quietly, whether the change moved on since (peek: it never writes one); if it did, the stale bar shows
|
|
1133
|
+
async function wtCheck(sid) {
|
|
1134
|
+
const k = wtKey(sid)
|
|
1135
|
+
if (wtUi.checking.has(k)) return
|
|
1136
|
+
wtUi.checking.add(k)
|
|
1137
|
+
try {
|
|
1138
|
+
const r = await api('POST', '/v1/sessions/' + encodeURIComponent(sid) + '/walkthrough', { scope: wtUi.scope.get(sid) || 'session', stale: true, peek: true }, { timeoutMs: 60000 })
|
|
1139
|
+
const had = wtUi.data.get(k)
|
|
1140
|
+
if (r && r.status === 'ok' && had && !wtUi.loading.has(k) && (r.fingerprint !== had.fingerprint || JSON.stringify(r.stale || null) !== JSON.stringify(had.stale || null))) {
|
|
1141
|
+
wtUi.data.set(k, r)
|
|
1142
|
+
if (wtUi.sid === sid) { wtUi.sig = ''; wtFill(wtSession(sid)) }
|
|
1143
|
+
}
|
|
1144
|
+
} catch {} finally { wtUi.checking.delete(k) }
|
|
1145
|
+
}
|
|
1125
1146
|
function wtClose() {
|
|
1126
1147
|
const sid = wtUi.sid
|
|
1127
1148
|
document.documentElement.classList.remove('wt-open')
|
|
@@ -1147,30 +1168,47 @@ async function wtSizeFetch(sid) {
|
|
|
1147
1168
|
try { data = await api('GET', '/v1/sessions/' + encodeURIComponent(sid) + '/change-size' + (sc === 'checkpoint' || sc === 'session' ? '?scope=' + sc : '')) } catch { data = e.data || null } // an older Mac: no line
|
|
1148
1169
|
wtSize.set(sid, { at: Date.now(), data, pending: false })
|
|
1149
1170
|
for (const slot of document.querySelectorAll('.fs-wt-size[data-id="' + CSS.escape(sid) + '"]')) {
|
|
1150
|
-
slot.innerHTML = sizeLineHtml(data, { esc, icon })
|
|
1171
|
+
slot.innerHTML = sizeLineHtml(data, { esc, icon, reviewed: wtRevCount(wtSession(sid), data) })
|
|
1151
1172
|
const b = slot.closest('.fs-wt'); if (b) b.classList.toggle('none', !!(data && data.files === 0))
|
|
1152
1173
|
}
|
|
1153
1174
|
if (loPop) loPop.refresh('wt', sid)
|
|
1154
1175
|
}
|
|
1176
|
+
// 2026-10-30 the sidebar's "3 of 7 reviewed": your marks on the walkthrough change-size names (pending ones too)
|
|
1177
|
+
function wtRevCount(s, size) {
|
|
1178
|
+
const fp = size && size.walk && size.walk.fp
|
|
1179
|
+
if (!s || !fp) return 0
|
|
1180
|
+
const rev = reviewedSet(s, fp)
|
|
1181
|
+
for (const x of wtUi.pending) if (x.fp === fp) { if (x.on) rev.add(x.step); else rev.delete(x.step) }
|
|
1182
|
+
return rev.size
|
|
1183
|
+
}
|
|
1184
|
+
function wtSizeRepaint(sid) {
|
|
1185
|
+
const data = (wtSize.get(sid) || {}).data
|
|
1186
|
+
for (const slot of document.querySelectorAll('.fs-wt-size[data-id="' + CSS.escape(sid) + '"]')) slot.innerHTML = sizeLineHtml(data, { esc, icon, reviewed: wtRevCount(wtSession(sid), data) })
|
|
1187
|
+
}
|
|
1155
1188
|
let loPop = null
|
|
1156
1189
|
// 0.9.17: nothing new since the last checkpoint → the whole session, with a quiet note ("No changes" only when that's empty too)
|
|
1157
1190
|
function wtFallBack(sid) { wtUi.scope.set(sid, 'session'); wtUi.fellBack.add(sid); wtRemember() }
|
|
1158
|
-
|
|
1191
|
+
// 2026-10-30 stale: true: with nothing written for this exact diff, the daemon hands back the newest
|
|
1192
|
+
// walkthrough it kept for this session and scope, marked stale, instead of a model run (Update: update)
|
|
1193
|
+
async function wtLoad(sid, { fresh = false, update = false } = {}) {
|
|
1159
1194
|
const k = wtKey(sid)
|
|
1160
1195
|
const had = wtUi.data.get(k)
|
|
1161
|
-
if (!fresh && had && had.status === 'empty' && wtUi.scope.get(sid) === 'checkpoint') { wtFallBack(sid); return wtLoad(sid) }
|
|
1162
|
-
if (wtUi.loading.has(k) || (wtUi.data.has(k) && !fresh)) return wtFill(wtSession(sid))
|
|
1196
|
+
if (!fresh && !update && had && had.status === 'empty' && wtUi.scope.get(sid) === 'checkpoint') { wtFallBack(sid); return wtLoad(sid) }
|
|
1197
|
+
if (wtUi.loading.has(k) || (wtUi.data.has(k) && !fresh && !update)) return wtFill(wtSession(sid))
|
|
1163
1198
|
wtUi.loading.add(k); wtUi.error.delete(k)
|
|
1164
1199
|
wtFill(wtSession(sid))
|
|
1165
1200
|
try {
|
|
1166
|
-
const r = await api('POST', '/v1/sessions/' + encodeURIComponent(sid) + '/walkthrough', { scope: wtUi.scope.get(sid) || 'session', ...(fresh ? { fresh: true } : {}) }, { timeoutMs: 480000 }) // the session's agent: up to ~3 min a read
|
|
1201
|
+
const r = await api('POST', '/v1/sessions/' + encodeURIComponent(sid) + '/walkthrough', { scope: wtUi.scope.get(sid) || 'session', ...(fresh ? { fresh: true } : {}), ...(fresh || update ? {} : { stale: true }) }, { timeoutMs: 480000 }) // the session's agent: up to ~3 min a read
|
|
1167
1202
|
wtUi.data.set(k, r)
|
|
1168
1203
|
if (r && r.status === 'ok') { const st = (r.steps || [])[0]; if (st && !wtUi.cur.has(k)) wtUi.cur.set(k, st.id) }
|
|
1204
|
+
// 2026-10-30 a new walkthrough comes with its marks (the ones carried over from the one before)
|
|
1205
|
+
if (r && r.status === 'ok' && Array.isArray(r.reviewedSteps) && r.fingerprint) for (const x of [wtSession(sid), state.sessions.find((y) => y.id === sid)]) if (x) x.reviewed = { ...(x.reviewed || {}), [r.fingerprint]: r.reviewedSteps }
|
|
1169
1206
|
if (r && r.status === 'no_range' && wtUi.scope.get(sid) === 'checkpoint') { wtUi.scope.set(sid, 'session'); wtUi.loading.delete(k); return wtLoad(sid) }
|
|
1170
1207
|
if (r && r.status === 'empty' && wtUi.scope.get(sid) === 'checkpoint') { wtFallBack(sid); wtUi.loading.delete(k); return wtLoad(sid) }
|
|
1171
1208
|
} catch (e) { wtUi.error.set(k, (e && e.message) || 'Couldn’t read the change.') }
|
|
1172
1209
|
wtUi.loading.delete(k)
|
|
1173
1210
|
if (wtUi.sid === sid) { wtUi.sig = ''; wtFill(wtSession(sid)) }
|
|
1211
|
+
const z = wtSize.get(sid); if (z && !z.pending) { z.at = 0; wtSizeFetch(sid) } // 2026-10-30 the sidebar's reviewed count follows the walkthrough just read
|
|
1174
1212
|
}
|
|
1175
1213
|
// The node for the page's #wtMount: built when what it shows changed, else moved back in.
|
|
1176
1214
|
function wtFill(s) {
|
|
@@ -1181,8 +1219,8 @@ function wtFill(s) {
|
|
|
1181
1219
|
const fp = data && data.fingerprint
|
|
1182
1220
|
const rev = reviewedSet(s, fp)
|
|
1183
1221
|
for (const x of wtUi.pending) if (x.fp === fp) { if (x.on) rev.add(x.step); else rev.delete(x.step) }
|
|
1184
|
-
const ui = { scope: wtUi.scope.get(s.id) || 'session', view: wtUi.view, filter: wtUi.filter, open: wtUi.code, groups: wtUi.groups, cur: wtUi.cur.get(k) || null, loading: wtUi.loading.has(k), error: wtUi.error.get(k) || null, ask: wtUi.ask, fellBack: wtUi.fellBack.has(s.id), deck: wtDeckOn(), slide: wtSlideGet(fp, wtUi.filter) }
|
|
1185
|
-
const sig = k + '|' + (data ? (data.fingerprint || data.status) + (data.cached ? 'c' : '') : '-') + '|' + JSON.stringify([ui.view, ui.filter, ui.loading, ui.error, ui.fellBack, ui.deck, [...wtUi.code], (data && data.scopes || []).map((x) => x.key + x.available).join()])
|
|
1222
|
+
const ui = { scope: wtUi.scope.get(s.id) || 'session', view: wtUi.view, filter: wtUi.filter, open: wtUi.code, groups: wtUi.groups, cur: wtUi.cur.get(k) || null, loading: wtUi.loading.has(k), error: wtUi.error.get(k) || null, ask: wtUi.ask, fellBack: wtUi.fellBack.has(s.id), deck: wtDeckOn(), slide: wtSlideGet(fp, wtUi.filter), sf: fp ? sfState(fp) : {}, split: wtSplitOn(), rail: wtRailOn(), spot: fp && wtUi.spot.has(fp) ? wtUi.spot.get(fp) : -1 }
|
|
1223
|
+
const sig = k + '|' + (data ? (data.fingerprint || data.status) + (data.cached ? 'c' : '') : '-') + '|' + JSON.stringify([ui.view, ui.filter, ui.loading, ui.error, ui.fellBack, ui.deck, [...wtUi.code], (data && data.scopes || []).map((x) => x.key + x.available).join(), ui.split, data && data.stale ? data.stale.fingerprint + ':' + data.stale.count : ''])
|
|
1186
1224
|
let node = wtUi.node
|
|
1187
1225
|
if (!node || wtUi.sig !== sig) {
|
|
1188
1226
|
// (the old node may be out of the page already, reading 0: what was recorded as you scrolled wins)
|
|
@@ -1196,9 +1234,10 @@ function wtFill(s) {
|
|
|
1196
1234
|
node.innerHTML = walkthroughHtml({ s, data, ui, reviewed: rev, esc, icon, when })
|
|
1197
1235
|
wtUi.node = node; wtUi.sig = sig
|
|
1198
1236
|
mount.replaceChildren(node)
|
|
1199
|
-
|
|
1237
|
+
wtHydrate(node, data)
|
|
1200
1238
|
wtStuck(document.querySelector('.wt-scroll')) // 0.9.18
|
|
1201
1239
|
wtDeckPaint(node, { first: true }) // 0.9.18 the slideshow: its progress, its dots in view
|
|
1240
|
+
wtSideWidth(node) // 2026-10-30 the file list's width, as you left it
|
|
1202
1241
|
const sc = node.querySelector('#wtScroll'); if (sc && keep) sc.scrollTop = keep
|
|
1203
1242
|
const sd = node.querySelector('.wt-side'); if (sd && keepSide) sd.scrollTop = keepSide
|
|
1204
1243
|
} else {
|
|
@@ -1211,6 +1250,7 @@ function wtFill(s) {
|
|
|
1211
1250
|
}
|
|
1212
1251
|
wtMarks(s)
|
|
1213
1252
|
}
|
|
1253
|
+
wtPicksPaint() // 2026-10-30 picked lines, their chips, the floating Ask
|
|
1214
1254
|
if (!data && !ui.loading && !ui.error) setTimeout(() => wtLoad(s.id), 0) // opened by a reload: read it now
|
|
1215
1255
|
}
|
|
1216
1256
|
// 0.9.18 where each scroll box in the walkthrough was (the slide or long page, the file list, the
|
|
@@ -1252,11 +1292,11 @@ function wtMarks(s) {
|
|
|
1252
1292
|
for (const g of node.querySelectorAll('.wt-ag[data-step] .wt-ag-ok')) g.hidden = !rev.has(g.closest('.wt-ag').dataset.step)
|
|
1253
1293
|
const p = node.querySelector('#wtProg'); if (p) p.textContent = (data.steps || []).filter((x) => rev.has(x.id)).length + ' of ' + (data.steps || []).length + ' reviewed'
|
|
1254
1294
|
}
|
|
1255
|
-
async function wtReviewed(sid, step) {
|
|
1295
|
+
async function wtReviewed(sid, step, { force = null, advance = true } = {}) {
|
|
1256
1296
|
const s = wtSession(sid)
|
|
1257
1297
|
const data = wtUi.data.get(wtKey(sid))
|
|
1258
1298
|
if (!s || !data || !data.fingerprint) return
|
|
1259
|
-
const on = !reviewedSet(s, data.fingerprint).has(step) || wtUi.pending.some((x) => x.fp === data.fingerprint && x.step === step && !x.on)
|
|
1299
|
+
const on = force != null ? force : !reviewedSet(s, data.fingerprint).has(step) || wtUi.pending.some((x) => x.fp === data.fingerprint && x.step === step && !x.on)
|
|
1260
1300
|
const x = { fp: data.fingerprint, step, on }
|
|
1261
1301
|
wtUi.pending = wtUi.pending.filter((p) => !(p.fp === x.fp && p.step === step)).concat([x])
|
|
1262
1302
|
wtMarks(s)
|
|
@@ -1268,7 +1308,20 @@ async function wtReviewed(sid, step) {
|
|
|
1268
1308
|
} catch (e) { toast(e.message, 4200) }
|
|
1269
1309
|
wtUi.pending = wtUi.pending.filter((p) => p !== x)
|
|
1270
1310
|
wtMarks(s)
|
|
1271
|
-
|
|
1311
|
+
wtSizeRepaint(sid) // 2026-10-30 the sidebar's reviewed count
|
|
1312
|
+
if (on && advance) { const nx = stepAfter(data.steps || [], step, 1, wtUi.filter); if (nx && nx !== step) wtGo(sid, nx) }
|
|
1313
|
+
}
|
|
1314
|
+
// 0.9.19 (Joel): "Mark reviewed & next" in the slides' bar: marks the step on screen (never
|
|
1315
|
+
// un-marks it) and moves on; on the last step it finishes on the overview.
|
|
1316
|
+
function wtRevNext() {
|
|
1317
|
+
const node = wtUi.node, deck = node && node.querySelector('#wtDeck'); if (!deck) return
|
|
1318
|
+
const slides = [...deck.children], cur = wtDeckCur()
|
|
1319
|
+
const st = revNextState(slides.map((x) => ({ step: x.dataset.step || null })), cur)
|
|
1320
|
+
if (!st.show) return void wtSlideGo(cur + 1)
|
|
1321
|
+
const step = slides[cur].dataset.step, sid = wtUi.sid, s = wtSession(sid), data = wtUi.data.get(wtKey(sid))
|
|
1322
|
+
const done = s && data && (reviewedSet(s, data.fingerprint).has(step) || wtUi.pending.some((x) => x.fp === data.fingerprint && x.step === step && x.on)) && !wtUi.pending.some((x) => data && x.fp === data.fingerprint && x.step === step && !x.on)
|
|
1323
|
+
if (!done) wtReviewed(sid, step, { force: true, advance: false })
|
|
1324
|
+
wtSlideGo(st.to)
|
|
1272
1325
|
}
|
|
1273
1326
|
function wtGo(sid, step, { group = null } = {}) {
|
|
1274
1327
|
const node = wtUi.node; if (!node) return
|
|
@@ -1315,6 +1368,8 @@ function wtSlideGo(i) {
|
|
|
1315
1368
|
if (nav) {
|
|
1316
1369
|
nav.querySelector('.wt-dprev').disabled = i === 0
|
|
1317
1370
|
nav.querySelector('.wt-dnext').disabled = i === n - 1
|
|
1371
|
+
const rv = nav.querySelector('#wtDrev'), rs = revNextState(slides.map((x) => ({ step: x.dataset.step || null })), i)
|
|
1372
|
+
if (rv) { rv.hidden = !rs.show; const [l, sh] = revNextLabel(rs.last); rv.setAttribute('aria-label', l); rv.dataset.tip = rs.last ? REVFINISH_TIP : REVNEXT_TIP; const a1 = rv.querySelector('.wt-dl-l'), a2 = rv.querySelector('.wt-dl-s'); if (a1) a1.textContent = l; if (a2) a2.textContent = sh }
|
|
1318
1373
|
const now = nav.querySelector('#wtDnow'); if (now) now.textContent = (slides[i].getAttribute('aria-label') || '').replace(/^Step \d+ of \d+: /, '')
|
|
1319
1374
|
const c = nav.querySelector('#wtDcount'); if (c) c.textContent = (i + 1) + ' / ' + n
|
|
1320
1375
|
;[...nav.querySelectorAll('.wt-dot')].forEach((d, j) => { d.classList.toggle('cur', j === i); if (j === i) d.setAttribute('aria-current', 'step'); else d.removeAttribute('aria-current') })
|
|
@@ -1322,7 +1377,7 @@ function wtSlideGo(i) {
|
|
|
1322
1377
|
if (sc) sc.scrollTop = wtSlideScroll.has(fp + '|' + i) ? wtSlideScroll.get(fp + '|' + i) : Math.min(sc.scrollTop, deck.offsetTop)
|
|
1323
1378
|
wtSlideSet(fp, wtUi.filter, i)
|
|
1324
1379
|
wtDeckPaint(node)
|
|
1325
|
-
|
|
1380
|
+
wtHydrate(node, data)
|
|
1326
1381
|
wtStuck(sc)
|
|
1327
1382
|
return true
|
|
1328
1383
|
}
|
|
@@ -1338,12 +1393,30 @@ function wtStep(dir) {
|
|
|
1338
1393
|
// "Ask about this change…": the question (with the step's file:line) into the session's own chat box, not sent.
|
|
1339
1394
|
function wtAsk(sid, text) {
|
|
1340
1395
|
text = String(text || '').trim()
|
|
1341
|
-
if (!text) return
|
|
1342
1396
|
const data = wtUi.data.get(wtKey(sid))
|
|
1397
|
+
// 2026-10-30 lines you picked: each ref and its exact lines go in with the question (ui/walk-lines.js)
|
|
1398
|
+
const picks = wtPicksNow(data)
|
|
1399
|
+
if (picks.length) {
|
|
1400
|
+
const msg = refsPrompt(describePicks(picks, (p) => { const x = patchFor(data, p.k); return x && { rows: rowsOf(x.patch), label: x.label, stepTitle: x.stepTitle } }), text)
|
|
1401
|
+
wtUi.picks = []
|
|
1402
|
+
return wtAskPut(sid, msg)
|
|
1403
|
+
}
|
|
1404
|
+
if (!text) return
|
|
1343
1405
|
const st = data && data.status === 'ok' ? (data.steps || []).find((x) => x.id === wtUi.cur.get(wtKey(sid))) : null
|
|
1344
1406
|
const co = st && st.callouts[0]
|
|
1345
1407
|
const where = st ? (co ? co.file + ':' + co.line : st.files[0] + (st.snippets[0] ? ':' + ((/^@@ -\d+(?:,\d+)? \+(\d+)/.exec(st.snippets[0].patch) || [])[1] || '') : '')).replace(/:$/, '') : ''
|
|
1346
1408
|
const msg = (where ? 'About `' + where + '`' + (st ? ' (' + st.title + ')' : '') + ': ' : '') + text
|
|
1409
|
+
wtAskPut(sid, msg)
|
|
1410
|
+
}
|
|
1411
|
+
function wtAskPut(sid, msg) {
|
|
1412
|
+
// 0.9.17 a Codex session in the board's terminal: the question goes into Codex's own box there (pasted, not sent)
|
|
1413
|
+
const cs = wtSession(sid)
|
|
1414
|
+
if (cs && cs.codingAgent === 'codex' && cs.terminal && cs.terminal.kind === 'leftoff' && cs.terminal.termId) {
|
|
1415
|
+
const tid = cs.terminal.termId
|
|
1416
|
+
wtUi.ask = ''; wtClose()
|
|
1417
|
+
terminal.show(tid).then((ok) => ok && api('POST', '/v1/term/' + tid + '/in', { data: '\x1b[200~' + msg + '\x1b[201~' })).then((r) => toast(r ? 'In Codex’s box: press Enter there to send' : 'Its terminal isn’t running any more', 4000)).catch((e) => toast(e.message, 5000))
|
|
1418
|
+
return
|
|
1419
|
+
}
|
|
1347
1420
|
const k = 'live:' + sid
|
|
1348
1421
|
const typed = ((state.drafts && state.drafts[k]) || '').trim()
|
|
1349
1422
|
state.drafts[k] = typed ? typed + '\n\n' + msg : msg
|
|
@@ -1361,6 +1434,233 @@ function wtExport(kind) {
|
|
|
1361
1434
|
const a = document.createElement('a'); a.href = url; a.download = exportName(title); document.body.appendChild(a); a.click(); a.remove()
|
|
1362
1435
|
setTimeout(() => URL.revokeObjectURL(url), 4000)
|
|
1363
1436
|
}
|
|
1437
|
+
// ---------------------------------------------------------------- 2026-10-30 walkthrough round 3
|
|
1438
|
+
// Line refs for Ask (asked: "reference line(s) in a file or multiple files for the ask"): a line
|
|
1439
|
+
// number picks it; shift-click or a drag across numbers, a range; Cmd/Ctrl-click adds another (any
|
|
1440
|
+
// file, any step). On a phone: tap to pick, tap another line of the same snippet to stretch it.
|
|
1441
|
+
// The picks show as chips over the ask box (× takes one off, the chip goes to its lines) and a
|
|
1442
|
+
// floating "Ask about these lines" sits by the newest one.
|
|
1443
|
+
function wtPicksNow(data) {
|
|
1444
|
+
const fp = data && data.fingerprint
|
|
1445
|
+
if (!fp || wtUi.picksFp !== fp) { wtUi.picks = []; wtUi.picksFp = fp || '' }
|
|
1446
|
+
return wtUi.picks
|
|
1447
|
+
}
|
|
1448
|
+
function wtGutHit(target) {
|
|
1449
|
+
const g = target && target.closest && target.closest('.wt-gd .gd-gut')
|
|
1450
|
+
if (!g || g.classList.contains('gd-span') || !wtUi.node || !wtUi.node.contains(g)) return null
|
|
1451
|
+
const row = g.closest('[data-ri]'), gd = g.closest('.wt-gd')
|
|
1452
|
+
if (!row || !gd || !g.textContent) return null // an empty number: the other side of a split row
|
|
1453
|
+
const data = wtUi.data.get(wtKey(wtUi.sid))
|
|
1454
|
+
const x = patchFor(data, gd.dataset.wtk)
|
|
1455
|
+
if (!x) return null
|
|
1456
|
+
return { k: gd.dataset.wtk, file: x.file, step: x.step, ri: Number(row.dataset.ri), gd }
|
|
1457
|
+
}
|
|
1458
|
+
let wtDrag = null, wtPtr = 'mouse'
|
|
1459
|
+
document.addEventListener('pointerdown', (ev) => {
|
|
1460
|
+
wtPtr = ev.pointerType || 'mouse'
|
|
1461
|
+
if (wtPtr !== 'mouse' || ev.button !== 0) return
|
|
1462
|
+
const at = wtGutHit(ev.target)
|
|
1463
|
+
if (!at) return
|
|
1464
|
+
ev.preventDefault() // no text selection while picking
|
|
1465
|
+
const data = wtUi.data.get(wtKey(wtUi.sid))
|
|
1466
|
+
const before = wtPicksNow(data)
|
|
1467
|
+
wtUi.picks = pickLine(before, at, { shift: ev.shiftKey, add: ev.metaKey || ev.ctrlKey })
|
|
1468
|
+
const i = wtUi.picks.findIndex((p) => p.k === at.k && at.ri >= p.a && at.ri <= p.b)
|
|
1469
|
+
wtDrag = !ev.shiftKey && i >= 0 && wtUi.picks.length >= before.length ? { i, k: at.k } : null
|
|
1470
|
+
wtPicksPaint({ focusAsk: false })
|
|
1471
|
+
})
|
|
1472
|
+
document.addEventListener('pointermove', (ev) => {
|
|
1473
|
+
if (!wtDrag || !(ev.buttons & 1)) return
|
|
1474
|
+
const el = document.elementFromPoint(ev.clientX, ev.clientY)
|
|
1475
|
+
const row = el && el.closest && el.closest('.wt-gd[data-wtk] [data-ri]')
|
|
1476
|
+
if (!row || row.closest('.wt-gd').dataset.wtk !== wtDrag.k) return
|
|
1477
|
+
const next = dragTo(wtUi.picks, wtDrag.i, Number(row.dataset.ri))
|
|
1478
|
+
const p = next[wtDrag.i], q = wtUi.picks[wtDrag.i]
|
|
1479
|
+
if (p && q && (p.a !== q.a || p.b !== q.b)) { wtUi.picks = next; wtPicksPaint() }
|
|
1480
|
+
})
|
|
1481
|
+
document.addEventListener('pointerup', () => { wtDrag = null })
|
|
1482
|
+
document.addEventListener('click', (ev) => {
|
|
1483
|
+
if (wtPtr === 'mouse') return
|
|
1484
|
+
const at = wtGutHit(ev.target)
|
|
1485
|
+
if (!at) return
|
|
1486
|
+
ev.preventDefault()
|
|
1487
|
+
wtUi.picks = pickLine(wtPicksNow(wtUi.data.get(wtKey(wtUi.sid))), at, { touch: true })
|
|
1488
|
+
wtPicksPaint()
|
|
1489
|
+
}, true)
|
|
1490
|
+
// the picked rows marked (both drawings), the chips, the floating button
|
|
1491
|
+
function wtPicksPaint() {
|
|
1492
|
+
const node = wtUi.node
|
|
1493
|
+
if (!node) return
|
|
1494
|
+
const data = wtUi.data.get(wtKey(wtUi.sid))
|
|
1495
|
+
const picks = wtPicksNow(data)
|
|
1496
|
+
for (const r of node.querySelectorAll('.wt-sel')) r.classList.remove('wt-sel', 'wt-sel-a', 'wt-sel-b')
|
|
1497
|
+
for (const gd of node.querySelectorAll('.wt-gd[data-wtk]')) {
|
|
1498
|
+
const mine = picks.filter((p) => p.k === gd.dataset.wtk)
|
|
1499
|
+
if (!mine.length) continue
|
|
1500
|
+
for (const r of gd.querySelectorAll('[data-ri]')) {
|
|
1501
|
+
const ri = Number(r.dataset.ri)
|
|
1502
|
+
const p = mine.find((x) => ri >= x.a && ri <= x.b)
|
|
1503
|
+
if (!p || r.classList.contains('gd-hunk') || r.classList.contains('gd-note')) continue
|
|
1504
|
+
r.classList.add('wt-sel')
|
|
1505
|
+
if (ri === p.a) r.classList.add('wt-sel-a')
|
|
1506
|
+
if (ri === p.b) r.classList.add('wt-sel-b')
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
const tray = node.querySelector('#wtRefs')
|
|
1510
|
+
if (tray) {
|
|
1511
|
+
const refs = describePicks(picks, (p) => { const x = patchFor(data, p.k); return x && { rows: rowsOf(x.patch), label: x.label } })
|
|
1512
|
+
tray.innerHTML = refs.map((r, i) => '<span class="wt-ref" role="listitem"><button type="button" class="wt-ref-go mono" data-action="wt-ref-go" data-i="' + i + '" data-tip="Show these lines">' + esc(r.label) + '</button><button type="button" class="wt-ref-x" data-action="wt-ref-x" data-i="' + i + '" aria-label="' + esc('Remove ' + r.label) + '" data-tip="Remove">' + icon('x') + '</button></span>').join('') +
|
|
1513
|
+
(refs.length > 1 ? '<button type="button" class="linkish wt-ref-clr" data-action="wt-ref-x" data-i="all">Clear</button>' : '')
|
|
1514
|
+
tray.hidden = !refs.length
|
|
1515
|
+
const inp = node.querySelector('#wtAsk')
|
|
1516
|
+
if (inp) inp.placeholder = refs.length ? (refs.length === 1 ? 'Ask about these lines…' : 'Ask about these ' + refs.length + ' places…') : 'Ask about this change…'
|
|
1517
|
+
}
|
|
1518
|
+
wtAskLPlace()
|
|
1519
|
+
}
|
|
1520
|
+
// the floating "Ask about these lines": under the newest pick's last row, when that row is on screen
|
|
1521
|
+
function wtAskLPlace() {
|
|
1522
|
+
const node = wtUi.node, b = node && node.querySelector('#wtAskL')
|
|
1523
|
+
if (!b) return
|
|
1524
|
+
// the newest pick that's on this slide or page
|
|
1525
|
+
let gd = null, row = null
|
|
1526
|
+
for (let i = wtUi.picks.length - 1; i >= 0 && !row; i--) {
|
|
1527
|
+
const p = wtUi.picks[i]
|
|
1528
|
+
gd = node.querySelector('.wt-gd[data-wtk="' + CSS.escape(p.k) + '"]')
|
|
1529
|
+
row = gd && [...gd.querySelectorAll('[data-ri="' + p.b + '"]')].find((x) => x.getClientRects().length)
|
|
1530
|
+
}
|
|
1531
|
+
const sc = node.querySelector('#wtScroll')
|
|
1532
|
+
const r = row && row.getBoundingClientRect(), box = sc && sc.getBoundingClientRect(), g = gd && gd.getBoundingClientRect()
|
|
1533
|
+
if (!r || !box || r.bottom < box.top || r.top > box.bottom - 40) { b.hidden = true; return }
|
|
1534
|
+
b.hidden = false
|
|
1535
|
+
const w = b.offsetWidth || 200
|
|
1536
|
+
b.style.top = Math.round(Math.min(box.bottom - 48, r.bottom + 4)) + 'px'
|
|
1537
|
+
b.style.left = Math.round(Math.max(box.left + 8, Math.min(g.right - w - 8, window.innerWidth - w - 8))) + 'px'
|
|
1538
|
+
}
|
|
1539
|
+
document.addEventListener('scroll', (e) => { if (wtUi.node && wtUi.picks.length && e.target && e.target.nodeType === 1 && wtUi.node.contains(e.target)) requestAnimationFrame(wtAskLPlace) }, { capture: true, passive: true })
|
|
1540
|
+
window.addEventListener('resize', () => { if (wtUi.node && wtUi.picks.length) requestAnimationFrame(wtAskLPlace) })
|
|
1541
|
+
// a drawn patch's key and a row -> on screen: the step's slide or section, its code and its file open, scrolled to
|
|
1542
|
+
function wtReveal(key, ri, { flash = false } = {}) {
|
|
1543
|
+
const sid = wtUi.sid, data = sid && wtUi.data.get(wtKey(sid))
|
|
1544
|
+
if (!data || !wtUi.node) return null
|
|
1545
|
+
const k = String(key)
|
|
1546
|
+
if (k.startsWith('s:')) {
|
|
1547
|
+
const step = k.split(':')[1]
|
|
1548
|
+
const st = (data.steps || []).find((x) => x.id === step)
|
|
1549
|
+
if (wtUi.view !== 'story') { wtUi.view = 'story'; wtRemember(); wtFill(wtSession(sid)) }
|
|
1550
|
+
if (st && st.tag !== 'Core' && !wtUi.code.has(step) && wtDeckCur() < 0) { wtUi.code.add(step); wtFill(wtSession(sid)) }
|
|
1551
|
+
wtGo(sid, step)
|
|
1552
|
+
const slot = wtUi.node.querySelector('[data-snip="' + CSS.escape(k.slice(2)) + '"]')
|
|
1553
|
+
const d = slot && slot.closest('details.wt-sf')
|
|
1554
|
+
if (d && !d.open) { d.open = true; sfSet(data.fingerprint, [d.dataset.sfk], true); wtHydrate(wtUi.node, data) }
|
|
1555
|
+
} else if (k.startsWith('f:')) {
|
|
1556
|
+
if (wtUi.view !== 'files') { wtUi.view = 'files'; wtRemember(); wtFill(wtSession(sid)) }
|
|
1557
|
+
const i = (data.files || []).findIndex((f) => f.path === k.slice(2))
|
|
1558
|
+
const d = wtUi.node.querySelector('details.wt-file[data-file-i="' + i + '"]')
|
|
1559
|
+
if (d && !d.open) { d.open = true; wtHydrate(wtUi.node, data) }
|
|
1560
|
+
}
|
|
1561
|
+
wtPicksPaint()
|
|
1562
|
+
const gd = wtUi.node.querySelector('.wt-gd[data-wtk="' + CSS.escape(k) + '"]')
|
|
1563
|
+
const row = gd && gd.querySelector('[data-ri="' + ri + '"]')
|
|
1564
|
+
if (row) row.scrollIntoView({ block: 'center', behavior: reducedMotion() ? 'auto' : 'smooth' })
|
|
1565
|
+
return row
|
|
1566
|
+
}
|
|
1567
|
+
// Next spot (asked: "some sort of jump to next spot to really look at button"): the button and n
|
|
1568
|
+
// (Shift+N back) walk ui/walk-spots.js's spots across steps; the reason shows as a tag at the line
|
|
1569
|
+
function wtSpots(data) { return data && data.status === 'ok' ? spotsFor(data, readingOrder(data.steps, data)) : [] }
|
|
1570
|
+
function wtSpotGo(dir = 1) {
|
|
1571
|
+
const sid = wtUi.sid, data = sid && wtUi.data.get(wtKey(sid))
|
|
1572
|
+
if (!data || data.status !== 'ok' || !wtUi.node) return
|
|
1573
|
+
const spots = wtSpots(data)
|
|
1574
|
+
if (!spots.length) return
|
|
1575
|
+
const cur = wtUi.spot.has(data.fingerprint) ? wtUi.spot.get(data.fingerprint) : -1
|
|
1576
|
+
const i = nextSpotIndex(spots.length, cur, dir)
|
|
1577
|
+
wtUi.spot.set(data.fingerprint, i)
|
|
1578
|
+
const sp = spots[i]
|
|
1579
|
+
const st = (data.steps || []).find((x) => x.id === sp.step)
|
|
1580
|
+
const sn = st && st.snippets[sp.snip]
|
|
1581
|
+
const rows = sn ? rowsOf(sn.patch) : []
|
|
1582
|
+
const ri = rows.findIndex((r) => (r.k === 'add' || r.k === 'del' || r.k === 'ctx') && (sp.side === 'old' ? r.o : r.n) === sp.line)
|
|
1583
|
+
for (const x of wtUi.node.querySelectorAll('.wt-spot-on')) x.classList.remove('wt-spot-on')
|
|
1584
|
+
for (const x of wtUi.node.querySelectorAll('.wt-spot-tag')) x.remove()
|
|
1585
|
+
const row = wtReveal('s:' + sp.step + ':' + sp.snip, ri)
|
|
1586
|
+
// in the split drawing a removed line is the left half, an added one the right
|
|
1587
|
+
const gd = row && row.closest('.wt-gd')
|
|
1588
|
+
const cands = gd ? [...gd.querySelectorAll('[data-ri="' + ri + '"]')] : []
|
|
1589
|
+
const at = cands.find((x) => x.classList.contains(sp.side === 'new' ? 'gd-hr' : 'gd-hl')) || cands[0] || null
|
|
1590
|
+
if (at) {
|
|
1591
|
+
at.classList.add('wt-spot-on')
|
|
1592
|
+
const tag = document.createElement('span')
|
|
1593
|
+
tag.className = 'wt-spot-tag'
|
|
1594
|
+
tag.setAttribute('role', 'note')
|
|
1595
|
+
tag.innerHTML = icon('crosshair') + '<span></span>'
|
|
1596
|
+
tag.lastChild.textContent = sp.why || 'Worth a look'
|
|
1597
|
+
at.appendChild(tag)
|
|
1598
|
+
}
|
|
1599
|
+
const n = wtUi.node.querySelector('#wtSpotN'); if (n) n.textContent = spotCountWords(spots.length, i)
|
|
1600
|
+
const b = wtUi.node.querySelector('.wt-spot'); if (b) b.setAttribute('aria-label', 'Next spot to look at, ' + spotCountWords(spots.length, i))
|
|
1601
|
+
}
|
|
1602
|
+
// Unified / Split (asked: "Should we offer a side by side diff view too?"): kept per device; split
|
|
1603
|
+
// only on a screen 1000px or wider, else unified (the toggle is hidden there)
|
|
1604
|
+
const wtWide = window.matchMedia ? window.matchMedia('(min-width: ' + SPLIT_MIN + 'px)') : null
|
|
1605
|
+
function wtSplitPref() { try { return localStorage.getItem('lo-wt-split') === '1' } catch { return false } }
|
|
1606
|
+
function wtSplitOn() { return wtSplitPref() && !!(wtWide && wtWide.matches) }
|
|
1607
|
+
if (wtWide && wtWide.addEventListener) wtWide.addEventListener('change', () => { if (wtUi.sid && wtSplitPref()) { wtUi.sig = ''; wtFill(wtSession(wtUi.sid)) } })
|
|
1608
|
+
function wtHydrate(node, data) { return hydrateWalkthrough(node, data, { renderDiff, renderSplit, split: wtSplitOn(), esc, icon }) }
|
|
1609
|
+
// the file list folds to a rail ([ or its button) and its edge drags; both kept per device
|
|
1610
|
+
function wtRailOn() { try { return localStorage.getItem('lo-wt-rail') === '1' } catch { return false } }
|
|
1611
|
+
function wtListUp() {
|
|
1612
|
+
const p = wtUi.sid && wtUi.node && wtUi.node.isConnected && wtUi.node.querySelector('#wt2p')
|
|
1613
|
+
if (!p || !p.getClientRects().length) return null
|
|
1614
|
+
const shown = (el) => el && getComputedStyle(el).display !== 'none'
|
|
1615
|
+
return shown(p.querySelector(':scope > .wt-side')) || shown(p.querySelector(':scope > .wt-rail')) ? p : null
|
|
1616
|
+
}
|
|
1617
|
+
function wtRail() {
|
|
1618
|
+
const p = wtListUp()
|
|
1619
|
+
if (!p) return
|
|
1620
|
+
const on = !p.classList.contains('rail')
|
|
1621
|
+
p.classList.toggle('rail', on)
|
|
1622
|
+
try { if (on) localStorage.setItem('lo-wt-rail', '1'); else localStorage.removeItem('lo-wt-rail') } catch {}
|
|
1623
|
+
const b = p.querySelector(on ? '.wt-rail .wt-railt' : '.wt-side .wt-railt')
|
|
1624
|
+
if (b && document.activeElement && document.activeElement.closest && document.activeElement.closest('.wt-railt')) b.focus({ preventScroll: true })
|
|
1625
|
+
}
|
|
1626
|
+
const clampW = (w) => Math.max(WT_SIDE_MIN, Math.min(WT_SIDE_MAX, Math.round(w)))
|
|
1627
|
+
function wtSideW() { try { const w = Number(localStorage.getItem('lo-wt-side-w')); return w ? clampW(w) : WT_SIDE_DEFAULT } catch { return WT_SIDE_DEFAULT } }
|
|
1628
|
+
function wtSideWidth(node, w = wtSideW()) {
|
|
1629
|
+
const p = node && node.querySelector('#wt2p')
|
|
1630
|
+
if (p) p.style.setProperty('--wt-side-w', w + 'px')
|
|
1631
|
+
const rz = p && p.querySelector('.wt-rz')
|
|
1632
|
+
if (rz) { rz.setAttribute('aria-valuemin', WT_SIDE_MIN); rz.setAttribute('aria-valuemax', WT_SIDE_MAX); rz.setAttribute('aria-valuenow', w) }
|
|
1633
|
+
}
|
|
1634
|
+
let wtRz = null
|
|
1635
|
+
document.addEventListener('pointerdown', (ev) => {
|
|
1636
|
+
const rz = ev.target && ev.target.closest && ev.target.closest('#wt2p > .wt-rz')
|
|
1637
|
+
if (!rz || ev.button !== 0) return
|
|
1638
|
+
ev.preventDefault()
|
|
1639
|
+
const side = rz.parentNode.querySelector('.wt-side')
|
|
1640
|
+
wtRz = { left: side.getBoundingClientRect().left, id: ev.pointerId, rz }
|
|
1641
|
+
try { rz.setPointerCapture(ev.pointerId) } catch {}
|
|
1642
|
+
rz.classList.add('on')
|
|
1643
|
+
})
|
|
1644
|
+
document.addEventListener('pointermove', (ev) => {
|
|
1645
|
+
if (!wtRz) return
|
|
1646
|
+
wtSideWidth(wtUi.node, clampW(ev.clientX - wtRz.left))
|
|
1647
|
+
})
|
|
1648
|
+
document.addEventListener('pointerup', () => {
|
|
1649
|
+
if (!wtRz) return
|
|
1650
|
+
const p = wtUi.node && wtUi.node.querySelector('#wt2p')
|
|
1651
|
+
const w = p ? parseInt(p.style.getPropertyValue('--wt-side-w'), 10) : 0
|
|
1652
|
+
if (w) try { localStorage.setItem('lo-wt-side-w', String(w)) } catch {}
|
|
1653
|
+
wtRz.rz.classList.remove('on')
|
|
1654
|
+
wtRz = null
|
|
1655
|
+
})
|
|
1656
|
+
document.addEventListener('keydown', (ev) => {
|
|
1657
|
+
const rz = ev.target && ev.target.closest && ev.target.closest('#wt2p > .wt-rz')
|
|
1658
|
+
if (!rz || (ev.key !== 'ArrowLeft' && ev.key !== 'ArrowRight')) return
|
|
1659
|
+
ev.preventDefault()
|
|
1660
|
+
const w = clampW(wtSideW() + (ev.key === 'ArrowRight' ? 16 : -16))
|
|
1661
|
+
try { localStorage.setItem('lo-wt-side-w', String(w)) } catch {}
|
|
1662
|
+
wtSideWidth(wtUi.node, w)
|
|
1663
|
+
})
|
|
1364
1664
|
document.addEventListener('submit', (ev) => {
|
|
1365
1665
|
const f = ev.target && ev.target.closest && ev.target.closest('form.wt-ask')
|
|
1366
1666
|
if (!f) return
|
|
@@ -1378,7 +1678,8 @@ document.addEventListener('toggle', (ev) => {
|
|
|
1378
1678
|
const d = ev.target
|
|
1379
1679
|
if (!d || !d.matches || !wtUi.node || !wtUi.node.contains(d)) return
|
|
1380
1680
|
if (d.matches('details.wt-grp')) { if (d.open) wtUi.groups.add(d.dataset.group); else wtUi.groups.delete(d.dataset.group) }
|
|
1381
|
-
if (d.
|
|
1681
|
+
if (d.matches('details.wt-sf')) { const data = wtUi.data.get(wtKey(wtUi.sid)); if (data && data.fingerprint) sfSet(data.fingerprint, [d.dataset.sfk], d.open) } // 2026-10-30 a step's file, folded or open, kept per walkthrough
|
|
1682
|
+
if (d.open) wtHydrate(wtUi.node, wtUi.data.get(wtKey(wtUi.sid)))
|
|
1382
1683
|
}, true)
|
|
1383
1684
|
// j / k between steps (not while typing)
|
|
1384
1685
|
document.addEventListener('keydown', (ev) => {
|
|
@@ -1386,12 +1687,16 @@ document.addEventListener('keydown', (ev) => {
|
|
|
1386
1687
|
const t = ev.target
|
|
1387
1688
|
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return
|
|
1388
1689
|
if (!document.getElementById('wtMount')) return
|
|
1690
|
+
// 2026-10-30 n / Shift+N: the next (previous) spot worth a real look; [ folds the file list
|
|
1691
|
+
if (ev.key === 'n' || ev.key === 'N') { ev.preventDefault(); return void wtSpotGo(ev.key === 'N' ? -1 : 1) }
|
|
1692
|
+
if (ev.key === '[') { ev.preventDefault(); return void wtRail() }
|
|
1389
1693
|
// 0.9.18 the slideshow: ← / → and j / k move, ↑ / ↓ scroll the slide, Home / End jump, Space pages
|
|
1390
1694
|
// (a focused button keeps its own Space and arrows; nothing is taken from a text box)
|
|
1391
1695
|
if (wtDeckCur() >= 0 && (!t || t === document.body || (wtUi.node && wtUi.node.contains(t)))) {
|
|
1392
1696
|
const sc = wtUi.node.querySelector('#wtScroll'), onCtl = t && t.closest && t.closest('button, a, summary, [role="radio"]')
|
|
1393
1697
|
const go = (i) => { ev.preventDefault(); wtSlideGo(i) }
|
|
1394
1698
|
const k = ev.key, cur = wtDeckCur()
|
|
1699
|
+
if ((k === 'ArrowRight' && ev.shiftKey) || (k === 'Enter' && !onCtl)) { ev.preventDefault(); return void wtRevNext() } // 0.9.19 mark reviewed & next
|
|
1395
1700
|
if (k === 'ArrowRight' || k === 'j') return go(cur + 1)
|
|
1396
1701
|
if (k === 'ArrowLeft' || k === 'k') return go(cur - 1)
|
|
1397
1702
|
if (k === 'Home' && !onCtl) return go(0)
|
|
@@ -2280,6 +2585,7 @@ function freshSession(s) {
|
|
|
2280
2585
|
if (op && out.headless) out = { ...out, headless: pendingHeadless(out.headless, op) }
|
|
2281
2586
|
return out
|
|
2282
2587
|
}
|
|
2588
|
+
const switchPending = new Set() // 1.0 a Continue in terminal / headless on its way
|
|
2283
2589
|
const fetchFresh = (id) => api('GET', '/v1/sessions/' + encodeURIComponent(id) + '/headless', undefined, { timeoutMs: 8000 }).then((r) => (r && r.fresh) || null, () => null)
|
|
2284
2590
|
// Redraw the session page from what it has (no fetch): the bar and the switch at once.
|
|
2285
2591
|
function repaintSession(id) {
|
|
@@ -2555,7 +2861,7 @@ function sessionPageHtml(s, { spv = null, live: liveV = null, lists = null } = {
|
|
|
2555
2861
|
const head = '<div class="pagehead s-head fs-head" data-sid="' + esc(s.id) + '"><div class="fs-head-row">' +
|
|
2556
2862
|
'<button type="button" class="fs-side-x" data-action="fs-side" aria-expanded="true" aria-controls="fsSide" aria-label="Hide the session panel" data-tip="Hide panel (⌘\\)">' + icon('chevron-right') + '</button>' +
|
|
2557
2863
|
title + (acts0 ? '<div class="fs-hacts">' + acts0 + '</div>' : '') + '</div>' +
|
|
2558
|
-
'<div class="sub">' + glyphFor(s, { cls: 'head-sg' }) + statusPill(s) + liveBadge(s) + (isDone(s) ? doneChipHtml(s, { icon, esc }) : archivedChipHtml(s)) + projChip(s) + wtChips(s) + (s.titleSource ? '<span class="s-tsrc">' + esc(s.titleSource === 'prompt' ? 'named from the first prompt' : s.titleSource === 'fallback' ? 'named from your prompt' : s.titleSource + ' title') + '</span>' : '') + '</div></div>' +
|
|
2864
|
+
'<div class="sub">' + glyphFor(s, { cls: 'head-sg' }) + statusPill(s) + liveBadge(s) + termPillHtml(s, { esc }) + (isDone(s) ? doneChipHtml(s, { icon, esc }) : archivedChipHtml(s)) + projChip(s) + wtChips(s) + (s.titleSource ? '<span class="s-tsrc">' + esc(s.titleSource === 'prompt' ? 'named from the first prompt' : s.titleSource === 'fallback' ? 'named from your prompt' : s.titleSource + ' title') + '</span>' : '') + '</div></div>' +
|
|
2559
2865
|
// 2026-10-29: the head above (title row, the status and Live chips) stays pinned while the panel scrolls; these scroll
|
|
2560
2866
|
(where.length ? '<div class="fs-where fs-repo" data-tip="' + esc(wherePath ? wherePath + (branch ? ' · ' + branch : '') : where.join(' · ')) + '">' + icon('folder') + '<span>' + esc(where.join(' · ')) + '</span></div>' : '') +
|
|
2561
2867
|
'<div class="fs-where">' + icon('monitor') + '<span>' + esc(mEmoji(s.machine) + ' ' + s.machine) + '</span><span class="faint">· ' + esc(ago(s.lastActivityAt)) + '</span></div>'
|
|
@@ -2568,7 +2874,7 @@ function sessionPageHtml(s, { spv = null, live: liveV = null, lists = null } = {
|
|
|
2568
2874
|
// else "Catch me up" with "Away 7h" as a small line under it (app.css, the fsside container); never clipped
|
|
2569
2875
|
const cuBtn = cu ? cu.replace('class="btn scu-btn"', 'class="btn ' + (away ? 'primary ' : '') + 'scu-btn fs-cu" data-pop="cu"' + (away ? ' data-away="1"' : '')).replace('Catch me up</button>', '<span class="fs-cu-l">' + (away ? '<span class="fs-away">' + esc(shortAway(awayWords(awayShownMs(s, awayMs)))) + '<span class="fs-away-dot"> · </span></span>' : '') + '<span class="fs-cu-v">Catch me up</span></span></button>') : ''
|
|
2570
2876
|
const wtOn = wtUi.sid === s.id
|
|
2571
|
-
const wtBtn = s.agent ? '' : walkButtonHtml(s, { esc, icon, on: wtOn, size: wtSizeOf(s.id) }) // 0.9.17; 0.9.18: a diff glyph, the size under it, a popover (ui/walk-pop.js)
|
|
2877
|
+
const wtBtn = s.agent ? '' : walkButtonHtml(s, { esc, icon, on: wtOn, size: wtSizeOf(s.id), reviewed: wtRevCount(s, wtSizeOf(s.id)) }) // 0.9.17; 0.9.18: a diff glyph, the size under it, a popover (ui/walk-pop.js)
|
|
2572
2878
|
// 0.9.17: Catch me up and Walk me through it side by side; Checkpoint now under them
|
|
2573
2879
|
const acts = '<div class="fs-acts' + (wtBtn ? ' fs-acts-cu' : '') + '">' + cuBtn + (wtBtn || (mine ? checkpointSplit(s, { quiet: away }) : '')) + '</div>' + (wtBtn && mine ? '<div class="fs-acts fs-acts-cp">' + checkpointSplit(s, { quiet: true }) + '</div>' : '')
|
|
2574
2880
|
const acts2 = '<div class="fs-acts2">' + (s.archived && mine ? unarchiveButtonHtml(s) : '') + goButton(s, { page: true }) + askButton(s, { page: true }) + (s.status === 'ended' || s.live === 'closed' ? resumeButton(s, s.viewer || { local: state.viewerLocal !== false, ghostty: !!state.ghostty }) : '') + '</div>'
|
|
@@ -2576,6 +2882,7 @@ function sessionPageHtml(s, { spv = null, live: liveV = null, lists = null } = {
|
|
|
2576
2882
|
side += '<section class="scu-wrap" id="session-catchup">' + sessionCatchupHtml(scuOf(s.id), { esc, icon, sid: s.id }) + '</section>' // 0.9.9 Catch me up on this session, at the top
|
|
2577
2883
|
side += '<div class="sask-wrap" id="session-ask">' + sAskPanel(s).html + '</div>' // 0.9.11 Ask about this session
|
|
2578
2884
|
side += ownerOfflineNote(s)
|
|
2885
|
+
side += termBarHtml(s, { esc, may: state.mayAnswer !== false }) // 1.0 in Leftoff's terminal: Continue headless
|
|
2579
2886
|
side += headlessBarHtml(s, { esc, icon, may: state.mayAnswer !== false, busy: modeBusy.get(s.id) || null, local: state.viewerLocal !== false, helpOpen: helpOpen.has(hmHelpId(s.id)) }) // 0.9.9: started from the board: Stop / Resume, Permissions
|
|
2580
2887
|
if (s.headless && s.headless.error) headlessErrors.set(s.id, s.headless.error)
|
|
2581
2888
|
// Checkpoints: a timeline, latest first, the latest open
|
|
@@ -2865,7 +3172,7 @@ function liveOpenHtml(s, v, sub) {
|
|
|
2865
3172
|
if (isFullLive(s.id)) {
|
|
2866
3173
|
const h = s.headless
|
|
2867
3174
|
const stop = h && h.state === 'running' && state.mayAnswer !== false ? '<button type="button" class="btn lf-stop" data-action="headless-stop" data-id="' + esc(s.id) + '">' + icon('square') + 'Stop</button>' : ''
|
|
2868
|
-
return '<section class="live-box full" id="live" aria-label="Live link, full screen" data-id="' + esc(s.id) + '">' + fullBarHtml(s.id, { esc, icon, glyph: glyphFor(s, { cls: 'lf-sg' }), pills: statusPill(s) + liveBadge(s) + mode, stop, retitle: retitleButtonHtml(s, { esc, icon, reach: reachOf(s), busy: retitling.has(s.id), cls: 'lf-retitle' }) }) +
|
|
3175
|
+
return '<section class="live-box full" id="live" aria-label="Live link, full screen" data-id="' + esc(s.id) + '">' + fullBarHtml(s.id, { esc, icon, glyph: glyphFor(s, { cls: 'lf-sg' }), pills: statusPill(s) + liveBadge(s) + termPillHtml(s, { esc }) + mode, stop, retitle: retitleButtonHtml(s, { esc, icon, reach: reachOf(s), busy: retitling.has(s.id), cls: 'lf-retitle' }) }) +
|
|
2869
3176
|
'<nav class="lf-tabs" id="lfTabs" aria-label="Open sessions"' + (lfTabsUi.html ? '' : ' hidden') + '>' + lfTabsUi.html + '</nav>' // 0.9.14: the open tabs stay in full screen (paintLfTabs fills it)
|
|
2870
3177
|
}
|
|
2871
3178
|
return '<section class="live-box" id="live" aria-label="Live link" data-id="' + esc(s.id) + '"><div class="live-head"><h2>Live</h2>' + mode + '<span class="faint">' + sub + '</span>' + fullButtonHtml(s.id, { esc, icon }) + '</div>'
|
|
@@ -4813,6 +5120,7 @@ function walkValueOf(w) {
|
|
|
4813
5120
|
const s = w.saved || null
|
|
4814
5121
|
const choice = s ? s.choice : w.choice
|
|
4815
5122
|
if (choice === 'agent') return 'agent'
|
|
5123
|
+
if (choice === 'codex') return 'codex' // 0.9.17
|
|
4816
5124
|
if (choice === 'claude') return 'claude:' + ((s && s.model) || 'sonnet')
|
|
4817
5125
|
if (choice === 'ollama') return 'ollama:' + ((s && s.model) || w.model || '')
|
|
4818
5126
|
return 'summarizer'
|
|
@@ -4854,7 +5162,7 @@ function paintWalk() {
|
|
|
4854
5162
|
let h = '', h0 = ''
|
|
4855
5163
|
// first: this session's own agent (recommended where Claude Code is here)
|
|
4856
5164
|
const ag = opt('agent')
|
|
4857
|
-
const fbLine = d.fallback ? 'When a session can’t be forked
|
|
5165
|
+
const fbLine = d.fallback ? 'When a session can’t be forked, ' + d.fallback.label + ' writes it' + (d.codexFound ? '; Codex sessions use Codex, and Codex stands in when Claude is unavailable.' : '.') : ''
|
|
4858
5166
|
h0 = card({ value: 'agent', on: !typed && kind === 'agent', name: ag.name, note: ag.line, suggested: d.claudeFound && d.suggested === 'agent', disabled: !d.claudeFound,
|
|
4859
5167
|
side: d.claudeFound ? '' : '<span class="badge wk-nf">○ not found here</span>', warn: d.claudeFound ? (kind === 'agent' && !typed ? fbLine : '') : 'Needs Claude Code on this machine.' })
|
|
4860
5168
|
const cl = opt('claude')
|
|
@@ -4869,6 +5177,10 @@ function paintWalk() {
|
|
|
4869
5177
|
h += h0
|
|
4870
5178
|
h += card({ value: 'claude:' + cm, on: claudeOn, name: cl.name, note: cl.line, suggested: d.claudeFound && d.suggested === 'claude', disabled: !d.claudeFound,
|
|
4871
5179
|
side: d.claudeFound ? '' : '<span class="badge wk-nf">○ not found here</span>', warn: d.claudeFound ? '' : 'Install Claude Code on this machine to use it.', extra: seg })
|
|
5180
|
+
// 0.9.17 Codex: read-only codex exec in the session's folder; Codex sessions use it by default, and Claude's stand-in when it's unavailable
|
|
5181
|
+
const cx = opt('codex')
|
|
5182
|
+
h += card({ value: 'codex', on: !typed && kind === 'codex', name: cx.name, note: cx.line, disabled: !d.codexFound,
|
|
5183
|
+
side: d.codexFound ? '' : '<span class="badge wk-nf">○ not found here</span>', warn: d.codexFound ? '' : 'Install Codex on this machine to use it.' })
|
|
4872
5184
|
const smo = opt('summarizer')
|
|
4873
5185
|
h += card({ value: 'summarizer', on: !typed && kind === 'summarizer', name: smo.name, note: smo.line, warn: d.summarizerSmall ? 'Small for this job: expect thinner walkthroughs.' : '' })
|
|
4874
5186
|
const ol = d.ollama || {}
|
|
@@ -6874,9 +7186,9 @@ async function startSend(btn) {
|
|
|
6874
7186
|
const box = btn.closest('.start-box')
|
|
6875
7187
|
const ta = box && box.querySelector('.start-text')
|
|
6876
7188
|
const text = ta ? ta.value.trim() : ''
|
|
6877
|
-
if (!text && !attachList(START_SID).length) { if (ta) ta.focus(); return }
|
|
6878
|
-
if (startUi.sending) return
|
|
6879
7189
|
const pick = startPick(startUi.data, startUi)
|
|
7190
|
+
if (!text && pick.agent !== 'codex' && !attachList(START_SID).length) { if (ta) ta.focus(); return } // 0.9.17 Codex may start with no first message
|
|
7191
|
+
if (startUi.sending) return
|
|
6880
7192
|
if (!pick.m || !pick.repo) return toast('Pick a Mac and a repo first')
|
|
6881
7193
|
startUi.sending = true; btn.disabled = true; btn.classList.add('sending'); btn.setAttribute('aria-busy', 'true')
|
|
6882
7194
|
try {
|
|
@@ -6885,14 +7197,21 @@ async function startSend(btn) {
|
|
|
6885
7197
|
await uploadsSettled(START_SID)
|
|
6886
7198
|
const atts = attachList(START_SID)
|
|
6887
7199
|
if (atts.some((a) => a.status === 'failed')) throw new Error('An attachment didn’t upload. Retry it or remove it.')
|
|
6888
|
-
const r = await api('POST', '/v1/sessions/start', startBody(pick, text, { draft: startUi.draft, atts }), { timeoutMs: 60000 })
|
|
7200
|
+
const r = await api('POST', '/v1/sessions/start', startBody(pick, text, { draft: pick.agent === 'codex' ? null : startUi.draft, atts: pick.agent === 'codex' ? [] : atts }), { timeoutMs: 60000 })
|
|
6889
7201
|
lastStartWrite(pick) // 0.9.13 ⌘K's New session… starts from here next time
|
|
6890
7202
|
if (atts.length) { attachUi.by.delete(START_SID); startUi.draft = null }
|
|
6891
7203
|
const xn = startExtrasNote(pick, r); if (xn) setTimeout(() => toast(xn, 6000), 1200) // 0.9.12: never a silent drop of a project's repo
|
|
6892
7204
|
if (state.drafts) delete state.drafts.start
|
|
6893
7205
|
if (!keepFocusAfterSend({ ok: true, touch: isTouchUi(window.matchMedia) })) letGoOfKeyboard(document) // 0.9.9: a phone's keyboard closes
|
|
6894
|
-
if (r.
|
|
6895
|
-
|
|
7206
|
+
if (r.app && !r.app.ok && r.app.message) setTimeout(() => toast(r.app.message, 7000), 900) // 1.0 My terminal app: it still runs (in the board's terminal)
|
|
7207
|
+
if (r.state === 'started' && r.sessionId) {
|
|
7208
|
+
startUi.ask = null; toast('Started on ' + (r.machine || pick.m.machine)); openStarted(r.sessionId, r.peerVia ? r.machine || pick.m.machine : null, text)
|
|
7209
|
+
if (r.run === 'board' && r.terminal && r.terminal.id && !r.peerVia) setTimeout(() => terminal.show(r.terminal.id).catch(() => {}), 50) // 1.0 Board terminal: shown now
|
|
7210
|
+
return
|
|
7211
|
+
}
|
|
7212
|
+
// 0.9.17 Codex: it runs in the board's terminal (shown now, on this Mac's board), and its session opens once Codex writes it
|
|
7213
|
+
if (r.agent === 'codex' && r.terminal && r.terminal.id && !r.peerVia) { toast('Codex started on ' + (r.machine || pick.m.machine)); if (r.run !== 'app' || !(r.app && r.app.ok)) terminal.show(r.terminal.id).catch(() => {}) }
|
|
7214
|
+
startUi.ask = { ...r, machine: r.machine || pick.m.machine, prompt: text }
|
|
6896
7215
|
pollStartAsk()
|
|
6897
7216
|
} catch (e) { toast(e.message, 6000) } finally { startUi.sending = false }
|
|
6898
7217
|
if (state.route && state.route.name === 'new') render()
|
|
@@ -8180,6 +8499,7 @@ document.addEventListener('click', async (ev) => {
|
|
|
8180
8499
|
if (action === 'wt-code') { const st = btn.dataset.step; if (wtUi.code.has(st)) wtUi.code.delete(st); else wtUi.code.add(st); return wtFill(wtSession(wtUi.sid)) }
|
|
8181
8500
|
if (action === 'wt-slide') { ev.preventDefault(); return void wtSlideGo(Number(btn.dataset.slide) || 0) } // 0.9.18
|
|
8182
8501
|
if (action === 'wt-dstep') return void wtSlideGo(wtDeckCur() + (Number(btn.dataset.dir) || 1))
|
|
8502
|
+
if (action === 'wt-revnext') return void wtRevNext()
|
|
8183
8503
|
if (action === 'wt-deck') { try { if (wtDeckOn()) localStorage.setItem('lo-wt-all', '1'); else localStorage.removeItem('lo-wt-all') } catch {} wtUi.sig = ''; return void wtFill(wtSession(wtUi.sid)) }
|
|
8184
8504
|
if (action === 'wt-fm-dir') { // 0.9.18 a folder in the file map folds; kept per walkthrough
|
|
8185
8505
|
const k = btn.parentElement && btn.parentElement.querySelector(':scope > .wt-fm-dk')
|
|
@@ -8205,6 +8525,22 @@ document.addEventListener('click', async (ev) => {
|
|
|
8205
8525
|
}
|
|
8206
8526
|
if (action === 'wt-go') { ev.preventDefault(); return wtGo(wtUi.sid, btn.dataset.step || null, { group: btn.dataset.group || null }) }
|
|
8207
8527
|
if (action === 'wt-step') return wtStep(Number(btn.dataset.dir) || 1)
|
|
8528
|
+
// 2026-10-30 walkthrough round 3
|
|
8529
|
+
if (action === 'wt-dmode') { try { if (btn.dataset.mode === 'split') localStorage.setItem('lo-wt-split', '1'); else localStorage.removeItem('lo-wt-split') } catch {} wtUi.sig = ''; return void wtFill(wtSession(wtUi.sid)) }
|
|
8530
|
+
if (action === 'wt-rail') return void wtRail()
|
|
8531
|
+
if (action === 'wt-spot') return void wtSpotGo(1)
|
|
8532
|
+
if (action === 'wt-update') { btn.disabled = true; return wtLoad(btn.dataset.id || wtUi.sid, { update: true }) }
|
|
8533
|
+
if (action === 'wt-sf-all') {
|
|
8534
|
+
const art = btn.closest('.wt-st'); const data = wtUi.data.get(wtKey(wtUi.sid)); if (!art || !data) return
|
|
8535
|
+
const open = btn.dataset.open === '1', ds = [...art.querySelectorAll('details.wt-sf')]
|
|
8536
|
+
for (const d of ds) d.open = open
|
|
8537
|
+
sfSet(data.fingerprint, ds.map((d) => d.dataset.sfk), open)
|
|
8538
|
+
if (open) wtHydrate(wtUi.node, data)
|
|
8539
|
+
return
|
|
8540
|
+
}
|
|
8541
|
+
if (action === 'wt-ref-x') { if (btn.dataset.i === 'all') wtUi.picks = []; else wtUi.picks = wtUi.picks.filter((_, i) => i !== Number(btn.dataset.i)); return void wtPicksPaint() }
|
|
8542
|
+
if (action === 'wt-ref-go') { const p = wtUi.picks[Number(btn.dataset.i)]; if (p) wtReveal(p.k, p.a); return }
|
|
8543
|
+
if (action === 'wt-ask-lines') { const inp = document.getElementById('wtAsk'); if (inp) { inp.focus({ preventScroll: true }); inp.scrollIntoView({ block: 'nearest' }) } return }
|
|
8208
8544
|
if (action === 'wt-export-html') return wtExport('html')
|
|
8209
8545
|
if (action === 'wt-export-md') return wtExport('md')
|
|
8210
8546
|
// 0.9.9: an info card on the session page (Directory, Worktree, Session id…): the whole value, a check for a moment
|
|
@@ -8428,7 +8764,7 @@ document.addEventListener('click', async (ev) => {
|
|
|
8428
8764
|
// 0.9.9 New session: the page's choices, the send, a yes or no for a start waiting here, Stop and Resume
|
|
8429
8765
|
if (action === 'start-pick') {
|
|
8430
8766
|
const k = btn.dataset.k, v = btn.dataset.v
|
|
8431
|
-
if (k === 'machine') { startUi.machine = v; startUi.repo = null; startUi.allRepos = false; startUi.browse = null; startUi.custom = null; Object.assign(startUi, { extra: [], adding: false, saving: false, find: '' }); if (attachList(START_SID).length) setTimeout(startReupload, 0) /* 0.9.13: its files go to that Mac */ } else if (k === 'project') { startUi.project = v || null; startUi.repo = null; startUi.drop = []; startUi.custom = null; startUi.browse = null; Object.assign(startUi, { extra: [], adding: false, saving: false }) } else if (k === 'repo') { startUi.repo = v; startUi.extra = startUi.extra.filter((x) => x !== v) } else if (k === 'where') startUi.where = v === 'worktree' ? 'worktree' : 'repo'; else if (k === 'mode') startUi.mode = v
|
|
8767
|
+
if (k === 'machine') { startUi.machine = v; startUi.repo = null; startUi.allRepos = false; startUi.browse = null; startUi.custom = null; Object.assign(startUi, { extra: [], adding: false, saving: false, find: '' }); if (attachList(START_SID).length) setTimeout(startReupload, 0) /* 0.9.13: its files go to that Mac */ } else if (k === 'project') { startUi.project = v || null; startUi.repo = null; startUi.drop = []; startUi.custom = null; startUi.browse = null; Object.assign(startUi, { extra: [], adding: false, saving: false }) } else if (k === 'repo') { startUi.repo = v; startUi.extra = startUi.extra.filter((x) => x !== v) } else if (k === 'where') startUi.where = v === 'worktree' ? 'worktree' : 'repo'; else if (k === 'mode') startUi.mode = v; else if (k === 'agent') startUi.agent = v === 'codex' ? 'codex' : 'claude' /* 0.9.17 */; else if (k === 'run') startUi.run = ['headless', 'board', 'app'].includes(v) ? v : null // 1.0
|
|
8432
8768
|
return render()
|
|
8433
8769
|
}
|
|
8434
8770
|
// 0.9.16 + Add another repo, and Save these as a project…
|
|
@@ -8507,6 +8843,21 @@ document.addEventListener('click', async (ev) => {
|
|
|
8507
8843
|
return render()
|
|
8508
8844
|
}
|
|
8509
8845
|
if (action === 'ask-auto') return autoRestClick(btn)
|
|
8846
|
+
// 1.0 Continue in terminal / Continue headless (src/run-switch.js)
|
|
8847
|
+
if (action === 'run-continue') {
|
|
8848
|
+
const run = btn.dataset.run === 'headless' ? 'headless' : 'board'
|
|
8849
|
+
if (!id || hlPending.has(id) || switchPending.has(id)) return
|
|
8850
|
+
switchPending.add(id); btn.disabled = true
|
|
8851
|
+
toast(run === 'headless' ? 'Ending it in the terminal, then continuing headless…' : 'Continuing in a terminal…', 3000)
|
|
8852
|
+
try {
|
|
8853
|
+
const r = await api('POST', '/v1/sessions/' + encodeURIComponent(id) + '/continue', { run }, { timeoutMs: 45000 })
|
|
8854
|
+
toast(run === 'headless' ? 'Continuing headless: talk to it here' : 'In the terminal now', 3500)
|
|
8855
|
+
if (r && r.terminal && r.terminal.id && run !== 'headless') terminal.show(r.terminal.id)
|
|
8856
|
+
} catch (e) { toast(e.message, 6000) }
|
|
8857
|
+
switchPending.delete(id)
|
|
8858
|
+
repaintSession(id)
|
|
8859
|
+
return render()
|
|
8860
|
+
}
|
|
8510
8861
|
if (action === 'headless-stop' || action === 'headless-run') {
|
|
8511
8862
|
const op = action === 'headless-stop' ? 'stop' : 'resume'
|
|
8512
8863
|
if (!id || hlPending.has(id)) return
|
|
@@ -9407,6 +9758,9 @@ async function needDismissClick(btn) {
|
|
|
9407
9758
|
|
|
9408
9759
|
// Go: "Switching…" at once, a note if it's slow, the result (with timings when slow) after.
|
|
9409
9760
|
function goClick(btn, id, s) {
|
|
9761
|
+
// 0.9.17 a session in the board's own terminal (a Codex start): show that terminal, here, on any device
|
|
9762
|
+
// 1.0 unless a terminal app is attached to it (leftoff attach): then Go brings that window forward on its Mac
|
|
9763
|
+
if (s && s.isLocal !== false && s.terminal && s.terminal.kind === 'leftoff' && s.terminal.termId && !(s.terminal.apps > 0 && state.viewerLocal !== false)) return terminal.show(s.terminal.termId).then((ok) => { if (!ok) toast('Its terminal in Leftoff isn’t running any more', 4000) })
|
|
9410
9764
|
return goWithFeedback({
|
|
9411
9765
|
id,
|
|
9412
9766
|
title: s && s.title,
|