@leftoff/cli 0.9.9-beta.5 → 0.9.9-beta.6
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 +2 -1
- package/bin/leftoff.js +262 -262
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +15 -2
- package/ui/app.js +43 -7
- package/ui/start.js +35 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leftoff",
|
|
3
|
-
"version": "0.9.9-beta.
|
|
3
|
+
"version": "0.9.9-beta.6",
|
|
4
4
|
"description": "Leftoff (formerly Checkpoints): WTF was I working on? Tracks every Claude Code session on this machine, writes a 3-line checkpoint when one goes quiet, and adds /cp (checkpoint now), /cp-name and a /leftoff pane. Talks to the local Leftoff daemon on 127.0.0.1:7429 and falls back to writing markdown in ~/checkpoints.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Joel"
|
package/mod/hooks/register.js
CHANGED
|
@@ -28,7 +28,7 @@ const END_TIMEOUT_MS = 1000
|
|
|
28
28
|
const MODEL_TIMEOUT_MS = 20000
|
|
29
29
|
|
|
30
30
|
// Module state. $.state is not used: the pane redraws with $.ui.invalidate.
|
|
31
|
-
const MOD_VERSION = '0.9.9-beta.
|
|
31
|
+
const MOD_VERSION = '0.9.9-beta.6' // keep in step with .claude-plugin/plugin.json
|
|
32
32
|
|
|
33
33
|
const st = {
|
|
34
34
|
internal: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leftoff/cli",
|
|
3
|
-
"version": "0.9.9-beta.
|
|
3
|
+
"version": "0.9.9-beta.6",
|
|
4
4
|
"description": "Leftoff: WTF was I working on? Tracks every Claude Code session, writes a 3-line checkpoint when one goes quiet, and gives you a daily digest and a board, on your Mac and (optionally) your phone.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/ui/app.css
CHANGED
|
@@ -2896,8 +2896,8 @@ del.gd-x { background: var(--gd-del-word); }
|
|
|
2896
2896
|
the restore banner): the words use the whole width, the buttons a row of
|
|
2897
2897
|
their own below, 44px tall */
|
|
2898
2898
|
@media (max-width: 640px) {
|
|
2899
|
-
.start-ask-banner > .grow, .restore-ask > .grow, .banner.restore:not(.open) > .grow { flex: 1 1 100%; }
|
|
2900
|
-
.start-ask-banner > .btn, .restore-ask > .btn, .banner.restore:not(.open) > .btn:not(.x) { flex: 1 1 0; justify-content: center; min-height: 44px; }
|
|
2899
|
+
.start-ask-banner > .grow, .restore-ask > .grow, .banner.restore:not(.open) > .grow, .pair-ask > .grow { flex: 1 1 100%; }
|
|
2900
|
+
.start-ask-banner > .btn, .restore-ask > .btn, .banner.restore:not(.open) > .btn:not(.x), .pair-ask > .btn { flex: 1 1 0; justify-content: center; min-height: 44px; }
|
|
2901
2901
|
.restore-ask > .rs-m { flex: 1 1 100%; }
|
|
2902
2902
|
}
|
|
2903
2903
|
|
|
@@ -2932,9 +2932,22 @@ del.gd-x { background: var(--gd-del-word); }
|
|
|
2932
2932
|
.sb-foot { display: flex; justify-content: flex-end; }
|
|
2933
2933
|
.sb-foot .btn { min-height: 44px; }
|
|
2934
2934
|
.sb-crumb:focus-visible, .sb-cut:focus-visible, .sb-dir:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2935
|
+
/* Go to a folder: type a path you know */
|
|
2936
|
+
.sb-path { display: flex; flex-direction: column; gap: 4px; margin: 0; }
|
|
2937
|
+
.sb-path-l { font-size: 12px; color: var(--muted); }
|
|
2938
|
+
.sb-path-row { display: flex; gap: 8px; min-width: 0; }
|
|
2939
|
+
.sb-path-in { flex: 1 1 auto; min-width: 0; min-height: 44px; padding: 0 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--card); color: var(--text); font: inherit; font-family: var(--mono); font-size: 16px; }
|
|
2940
|
+
.sb-path-in:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
2941
|
+
.sb-path-in[aria-invalid="true"] { border-color: var(--red); }
|
|
2942
|
+
.sb-path-row .btn { min-height: 44px; min-width: 44px; justify-content: center; }
|
|
2943
|
+
.sb-path-err { margin: 0; }
|
|
2935
2944
|
.start-custom .start-opt-main { white-space: normal; }
|
|
2936
2945
|
@media (max-width: 640px) {
|
|
2937
2946
|
.start-browse { padding: 10px; margin-left: -4px; margin-right: -4px; }
|
|
2938
2947
|
.sb-list { grid-template-columns: 1fr; max-height: none; overflow: visible; }
|
|
2939
2948
|
.sb-here .sb-use { flex: 1 1 100%; justify-content: center; }
|
|
2940
2949
|
}
|
|
2950
|
+
|
|
2951
|
+
/* 0.9.9 asked at pairing time: may other devices start sessions here? */
|
|
2952
|
+
.pair-ask .grow { min-width: 0; }
|
|
2953
|
+
@media (pointer: coarse) { .pair-ask > .btn { min-height: 44px; } }
|
package/ui/app.js
CHANGED
|
@@ -16,7 +16,7 @@ import { navHideStep, NAVHIDE_TYPING } from './navhide.js'
|
|
|
16
16
|
import { openLightbox } from './lightbox.js' // 0.9.8: the image preview
|
|
17
17
|
import { openPdfView, pdfThumb } from './pdfview.js' // 0.9.8: the PDF preview (pdf.js loads on the first PDF)
|
|
18
18
|
import { isBlank, composerKey, isTouchUi, composerHeight, composerBoxHtml } from './composer.js' // 0.9.8 the Live composer
|
|
19
|
-
import { startPageHtml, startPick, startAsksBanner, headlessBarHtml, startsSettingHtml, startViewerLocal as startViewerAt, startTexts, fillStartTexts, browseTarget } from './start.js' // 0.9.9 New session
|
|
19
|
+
import { startPageHtml, startPick, startAsksBanner, headlessBarHtml, startsSettingHtml, startViewerLocal as startViewerAt, startTexts, fillStartTexts, browseTarget, startPairAskHtml, typedPath } from './start.js' // 0.9.9 New session
|
|
20
20
|
import { attachPlan, skippedText, shortName, prepareFile, uploadFile, fetchAttachment, fmtSize, attachKind, pastedName, ATTACH_MAX_FILES } from './attach.js' // 0.9.8 attachments
|
|
21
21
|
import { wireTips } from './tips.js'
|
|
22
22
|
import { mountUpdateBar, takeCarry } from './update-bar.js' // 0.9.8 "Leftoff updated · Refresh"
|
|
@@ -841,6 +841,7 @@ async function viewBoard() {
|
|
|
841
841
|
'<a class="btn primary" href="#/catchup/new">' + icon('coffee') + 'Catch me up</a>' +
|
|
842
842
|
'<a class="btn" href="#/digest">' + icon('newspaper') + 'Today’s digest →</a></div></div>'
|
|
843
843
|
html += syncDetails() + staleLine(sessions)
|
|
844
|
+
html += startPairAskHtml(state.starts, { esc, local: startViewerLocal(state.starts && state.starts.viewer), busy: startUi.pairBusy }) // 0.9.9: asked once, at pairing time
|
|
844
845
|
html += restoreAsksBanner() + startAsksBanner(state.starts, { esc, icon, local: startViewerLocal(state.starts && state.starts.viewer), clock, busy: startUi.busy }) + restoreBanner()
|
|
845
846
|
// Needs you (v0.4.4): every session waiting on you, whatever the filters, longest wait first.
|
|
846
847
|
const needs = needsItems(sessions)
|
|
@@ -4176,24 +4177,48 @@ async function viewNewSession() {
|
|
|
4176
4177
|
const kf = fa && fa.classList && fa.classList.contains('start-text') ? { a: fa.selectionStart, b: fa.selectionEnd } : null
|
|
4177
4178
|
$app.innerHTML = startPageHtml(startUi.data, startUi, { esc, icon, emoji: mEmoji, local: startViewerLocal(startUi.data.viewer), clock, draft: (state.drafts && state.drafts.start) || '', busy: startUi.sending })
|
|
4178
4179
|
fillStartTexts($app, startTexts(startUi.data, startUi)) // folder names and paths: text, never markup
|
|
4180
|
+
{ const pi = $app.querySelector('.sb-path-in'); if (pi && startUi.browse && startUi.browse.typed && pi.value !== startUi.browse.typed) pi.value = startUi.browse.typed } // what you were typing survives a redraw
|
|
4179
4181
|
const ta = $app.querySelector('.start-text')
|
|
4180
4182
|
if (ta) { fitComposer(ta); if (kf && !ta.disabled) { ta.focus({ preventScroll: true }); try { ta.setSelectionRange(kf.a, kf.b) } catch {} } }
|
|
4181
4183
|
}
|
|
4182
4184
|
// Other folder…: list one folder on the picked Mac (that Mac's own disk, over the peer link for another Mac)
|
|
4183
|
-
async function startBrowse(machine, p) {
|
|
4185
|
+
async function startBrowse(machine, p, { typed = false } = {}) {
|
|
4184
4186
|
const seq = (startUi.browseSeq = (startUi.browseSeq || 0) + 1)
|
|
4185
|
-
|
|
4187
|
+
const prev = startUi.browse && startUi.browse.machine === machine ? startUi.browse : {}
|
|
4188
|
+
startUi.browse = { ...prev, machine, loading: true, error: null, pathError: null }
|
|
4186
4189
|
if (state.route && state.route.name === 'new') render()
|
|
4187
4190
|
try {
|
|
4188
4191
|
const d = await api('GET', '/v1/start/dirs?machine=' + encodeURIComponent(machine) + (p ? '&path=' + encodeURIComponent(p) : ''), undefined, { timeoutMs: 20000 })
|
|
4189
4192
|
if (seq !== startUi.browseSeq || !startUi.browse) return
|
|
4190
|
-
startUi.browse = { machine, loading: false, error: null, data: d }
|
|
4193
|
+
startUi.browse = { machine, loading: false, error: null, pathError: null, typed: typed ? '' : startUi.browse.typed, data: d }
|
|
4191
4194
|
} catch (e) {
|
|
4192
4195
|
if (seq !== startUi.browseSeq || !startUi.browse) return
|
|
4193
|
-
|
|
4196
|
+
// a typed path that isn't one you can open: said under the box, and the folder you were in stays
|
|
4197
|
+
startUi.browse = typed ? { ...startUi.browse, loading: false, pathError: e.message } : { ...startUi.browse, loading: false, error: e.message }
|
|
4198
|
+
}
|
|
4199
|
+
if (state.route && state.route.name === 'new') {
|
|
4200
|
+
await render() // the page re-reads before it redraws: focus after
|
|
4201
|
+
const pi = typed && startUi.browse.pathError ? $app.querySelector('.sb-path-in') : null
|
|
4202
|
+
const f = pi || (startUi.browseFocus ? $app.querySelector('.sb-crumb[aria-current]') : null)
|
|
4203
|
+
if (f) f.focus({ preventScroll: true })
|
|
4194
4204
|
}
|
|
4195
|
-
if (state.route && state.route.name === 'new') { render(); const f = $app.querySelector('.sb-crumb[aria-current]'); if (f && startUi.browseFocus) f.focus({ preventScroll: true }) }
|
|
4196
4205
|
}
|
|
4206
|
+
// "Go to a folder": Enter or Go takes the browser there (the Mac checks it like any other step)
|
|
4207
|
+
document.addEventListener('submit', (ev) => {
|
|
4208
|
+
const f = ev.target && ev.target.closest && ev.target.closest('form[data-form="start-browse-path"]')
|
|
4209
|
+
if (!f) return
|
|
4210
|
+
ev.preventDefault()
|
|
4211
|
+
const b = startUi.browse, inp = f.querySelector('.sb-path-in')
|
|
4212
|
+
const want = typedPath(inp ? inp.value : '')
|
|
4213
|
+
if (!b) return
|
|
4214
|
+
if (!want) { startUi.browse = { ...b, pathError: 'Type a folder, like ~/code/web.' }; render().then(() => { const pi = $app.querySelector('.sb-path-in'); if (pi) pi.focus() }); return }
|
|
4215
|
+
startUi.browseFocus = true
|
|
4216
|
+
startBrowse(b.machine, want, { typed: true })
|
|
4217
|
+
})
|
|
4218
|
+
document.addEventListener('input', (ev) => {
|
|
4219
|
+
if (!ev.target || !ev.target.classList || !ev.target.classList.contains('sb-path-in') || !startUi.browse) return
|
|
4220
|
+
startUi.browse.typed = ev.target.value // kept across the board's redraws
|
|
4221
|
+
})
|
|
4197
4222
|
// Claude Code's own words when a session it ran stopped with an error: text, never markup.
|
|
4198
4223
|
const headlessErrors = new Map()
|
|
4199
4224
|
function fillHeadlessErrors() {
|
|
@@ -5407,6 +5432,17 @@ document.addEventListener('click', async (ev) => {
|
|
|
5407
5432
|
return
|
|
5408
5433
|
}
|
|
5409
5434
|
if (action === 'start-all-repos') { startUi.allRepos = true; return render() }
|
|
5435
|
+
if (action === 'pair-ask-yes' || action === 'pair-ask-no') {
|
|
5436
|
+
if (startUi.pairBusy) return
|
|
5437
|
+
startUi.pairBusy = true
|
|
5438
|
+
try {
|
|
5439
|
+
const r = await api('POST', '/v1/starts/pair-ask', { yes: action === 'pair-ask-yes' })
|
|
5440
|
+
toast(r.answered === 'yes' ? 'Other devices can start sessions here' + (r.trustTurnedOn ? ' (Trust my paired devices is on too)' : '') : 'OK, they’ll ask here first', 4000)
|
|
5441
|
+
} catch (e) { toast(e.message, 5000) }
|
|
5442
|
+
startUi.pairBusy = false
|
|
5443
|
+
try { state.starts = await api('GET', '/v1/starts') } catch {}
|
|
5444
|
+
return render()
|
|
5445
|
+
}
|
|
5410
5446
|
if (action === 'start-again') { startUi.ask = null; return render() }
|
|
5411
5447
|
if (action === 'start-send') return startSend(btn)
|
|
5412
5448
|
if (action === 'start-ask-confirm' || action === 'start-ask-decline') {
|
|
@@ -6154,7 +6190,7 @@ document.addEventListener('submit', async (ev) => {
|
|
|
6154
6190
|
|
|
6155
6191
|
// Live updates over SSE; re-render the current view, at most every 800 ms.
|
|
6156
6192
|
let pending = null
|
|
6157
|
-
const busy = () => !!document.querySelector('.doc-viewer:not([hidden]), .tip:not([hidden]), .lo-menu, .title input, .repomore[open], .need .deny-form:not([hidden])') || !!(document.activeElement && document.activeElement.closest && document.activeElement.closest('.addform') && document.activeElement.value) || !!(document.activeElement && document.activeElement.closest && document.activeElement.closest('.need') && /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName)) || !!(document.activeElement && document.activeElement.closest && document.activeElement.closest('.live-box') && document.activeElement.value) || !!(document.activeElement && document.activeElement.closest && document.activeElement.closest('.start-box') && document.activeElement.value)
|
|
6193
|
+
const busy = () => !!document.querySelector('.doc-viewer:not([hidden]), .tip:not([hidden]), .lo-menu, .title input, .repomore[open], .need .deny-form:not([hidden])') || !!(document.activeElement && document.activeElement.closest && document.activeElement.closest('.addform') && document.activeElement.value) || !!(document.activeElement && document.activeElement.closest && document.activeElement.closest('.need') && /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName)) || !!(document.activeElement && document.activeElement.closest && document.activeElement.closest('.live-box') && document.activeElement.value) || !!(document.activeElement && document.activeElement.closest && document.activeElement.closest('.start-box') && document.activeElement.value) || !!(document.activeElement && document.activeElement.classList && document.activeElement.classList.contains('sb-path-in') && document.activeElement.value)
|
|
6158
6194
|
// Settings isn't redrawn by live updates: it would wipe what you're editing,
|
|
6159
6195
|
// and the QR code you're scanning (v0.4.6). Its sections fetch their own state.
|
|
6160
6196
|
function soon() { if (pending) return; pending = setTimeout(() => { pending = null; if (!busy() && !(state.route && ['settings', 'setup'].includes(state.route.name)) && !(state.route && state.route.name === 'catchup' && cuUi.open)) render(); refreshHealth() }, 800) }
|
package/ui/start.js
CHANGED
|
@@ -122,6 +122,12 @@ export function startPageHtml(data, ui = {}, { esc, icon, emoji = () => '', loca
|
|
|
122
122
|
export function startBrowseHtml(b, { esc, icon = () => '', machine = '' } = {}) {
|
|
123
123
|
let h = '<div class="start-browse" id="start-browse" role="region" aria-label="Folders on ' + esc(machine) + '">'
|
|
124
124
|
const d = b && b.data
|
|
125
|
+
// or type one you know (~/src/project, or a full path): the browser goes there; the same rules as walking to it
|
|
126
|
+
const goTo = '<form class="sb-path" data-form="start-browse-path" novalidate><label class="sb-path-l" for="sb-path-in">Go to a folder</label><div class="sb-path-row">' +
|
|
127
|
+
'<input type="text" id="sb-path-in" class="sb-path-in" name="path" placeholder="~/code/web" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="go"' + (b && b.pathError ? ' aria-invalid="true" aria-describedby="sb-path-err"' : '') + '>' +
|
|
128
|
+
'<button type="submit" class="btn">Go</button></div>' +
|
|
129
|
+
(b && b.pathError ? '<p class="start-rule bad sb-path-err" id="sb-path-err" role="alert" data-st="path-err"></p>' : '') + '</form>'
|
|
130
|
+
if (!d) h += goTo
|
|
125
131
|
if (d) {
|
|
126
132
|
h += '<nav class="sb-crumbs" aria-label="Folder">' + d.crumbs.map((c, i) => (i ? '<span class="sb-sep" aria-hidden="true">/</span>' : '') +
|
|
127
133
|
'<button type="button" class="sb-crumb" data-action="start-browse-go" data-crumb="' + i + '"' + (i === d.crumbs.length - 1 ? ' aria-current="location"' : '') + ' data-st="crumb-' + i + '"></button>').join('') + '</nav>'
|
|
@@ -129,6 +135,7 @@ export function startBrowseHtml(b, { esc, icon = () => '', machine = '' } = {})
|
|
|
129
135
|
(d.parent ? '<button type="button" class="btn sb-up" data-action="start-browse-up" aria-label="Up one folder">' + icon('chevron-left') + 'Up</button>' : '') +
|
|
130
136
|
'<span class="sb-cur"><span class="sb-cur-name" data-st="cur"></span>' + (d.git ? ' <span class="start-badge git">git</span>' : ' <span class="start-badge">not a git repo</span>') + '</span>' +
|
|
131
137
|
'<button type="button" class="btn primary sb-use" data-action="start-browse-use"' + (d.parent ? '' : ' disabled') + '>Use this folder</button></div>'
|
|
138
|
+
h += goTo
|
|
132
139
|
if (d.shortcuts && d.shortcuts.length > 1) h += '<div class="sb-cuts"><span class="sb-cuts-l">Jump to</span>' + d.shortcuts.map((c, i) => '<button type="button" class="sb-cut" data-action="start-browse-go" data-cut="' + i + '" data-st="cut-' + i + '"></button>').join('') + '</div>'
|
|
133
140
|
if (!d.entries.length) h += '<p class="start-empty">No folders in here.</p>'
|
|
134
141
|
else h += '<ul class="sb-list">' + d.entries.map((e, i) => '<li><button type="button" class="sb-dir" data-action="start-browse-go" data-entry="' + i + '">' + icon('folder') + '<span class="sb-name" data-st="entry-' + i + '"></span>' + (e.git ? '<span class="start-badge git">git</span>' : '') + icon('chevron-right', 'ic sb-go') + '</button></li>').join('') + '</ul>'
|
|
@@ -141,6 +148,17 @@ export function startBrowseHtml(b, { esc, icon = () => '', machine = '' } = {})
|
|
|
141
148
|
return h
|
|
142
149
|
}
|
|
143
150
|
|
|
151
|
+
// What you typed in "Go to a folder", as a path to ask for: no spaces around
|
|
152
|
+
// it, no quotes a copy brought along, a shell's "\ " back to a space, no
|
|
153
|
+
// trailing slash. "~" stays "~"; the Mac resolves it (src/dirs.js checkFolder).
|
|
154
|
+
export function typedPath(v) {
|
|
155
|
+
let t = String(v == null ? '' : v).trim()
|
|
156
|
+
if (/^(['"]).*\1$/.test(t) && t.length >= 2) t = t.slice(1, -1).trim()
|
|
157
|
+
t = t.replace(/\\ /g, ' ')
|
|
158
|
+
if (t.length > 1) t = t.replace(/\/+$/, '') || '/'
|
|
159
|
+
return t
|
|
160
|
+
}
|
|
161
|
+
|
|
144
162
|
// The text for every [data-st] on the page: names and paths from a Mac's disk.
|
|
145
163
|
export function startTexts(data, ui = {}) {
|
|
146
164
|
const t = {}
|
|
@@ -153,6 +171,7 @@ export function startTexts(data, ui = {}) {
|
|
|
153
171
|
;(d.shortcuts || []).forEach((c, i) => { t['cut-' + i] = c.label })
|
|
154
172
|
d.entries.forEach((e, i) => { t['entry-' + i] = e.name })
|
|
155
173
|
}
|
|
174
|
+
if (ui.browse && ui.browse.pathError) t['path-err'] = ui.browse.pathError
|
|
156
175
|
for (const a of (data && Array.isArray(data.pending) ? data.pending : [])) if (a.firstTime) t['ask-folder-' + a.id] = a.folder || ''
|
|
157
176
|
return t
|
|
158
177
|
}
|
|
@@ -189,6 +208,22 @@ export function startAsksBanner(info, { esc, icon, local = true, clock = (x) =>
|
|
|
189
208
|
}).join('')
|
|
190
209
|
}
|
|
191
210
|
|
|
211
|
+
// Asked once on THIS Mac's board when it pairs a phone or sees a new Mac:
|
|
212
|
+
// may it start sessions here? No is the default (first, and what doing
|
|
213
|
+
// nothing means); Yes turns on the switch, and Trust my paired devices if it's
|
|
214
|
+
// off (the words say so). Never shown off this Mac.
|
|
215
|
+
export function startPairAskHtml(info, { esc, local = true, busy = false } = {}) {
|
|
216
|
+
const a = info && info.pairAsk
|
|
217
|
+
if (!a || !local) return ''
|
|
218
|
+
const mac = info.machine || 'this Mac'
|
|
219
|
+
const who = a.kind === 'mac' ? (a.name || 'your other Mac') : 'your phone'
|
|
220
|
+
const what = a.kind === 'mac' ? 'Let ' + who + ' start sessions here?' : 'Let your phone start sessions here?'
|
|
221
|
+
const yes = 'Yes turns on “Allow starting sessions from other devices” for ' + mac + (info.trust ? '' : ', and Trust my paired devices so you can answer its prompts there') + '. It applies to every paired phone and Mac; change it any time in Settings → Privacy & security.'
|
|
222
|
+
return '<div class="banner info pair-ask" role="region" aria-label="' + esc(what) + '"><span class="grow"><b>' + esc(what) + '</b> ' + esc(a.kind === 'mac' ? who + ' could start Claude Code on ' + mac + ' with nobody here;' : 'It could start Claude Code on ' + mac + ' with nobody here;') + ' its edits and commands still wait for you in Needs you. ' + esc(yes) + '</span>' +
|
|
223
|
+
'<button type="button" class="btn" data-action="pair-ask-no"' + (busy ? ' aria-disabled="true"' : '') + '>No</button>' +
|
|
224
|
+
'<button type="button" class="btn" data-action="pair-ask-yes"' + (busy ? ' aria-disabled="true"' : '') + '>Yes, allow</button></div>'
|
|
225
|
+
}
|
|
226
|
+
|
|
192
227
|
// The session page's line for a session Leftoff runs: where it runs, who
|
|
193
228
|
// started it, and Stop / Resume. The error (Claude Code's own words) is put
|
|
194
229
|
// in with textContent by app.js ([data-headless-err]).
|