@leftoff/cli 0.9.9-beta.5 → 0.9.9-beta.7
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 +8 -2
- package/bin/leftoff.js +316 -308
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/core.js +37 -2
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +48 -2
- package/ui/app.js +112 -26
- package/ui/facts.js +34 -0
- package/ui/icons.js +1 -0
- package/ui/live-view.js +17 -0
- package/ui/start.js +52 -3
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]).
|
|
@@ -209,8 +244,22 @@ export function headlessBarHtml(s, { esc, icon, may = true } = {}) {
|
|
|
209
244
|
}
|
|
210
245
|
|
|
211
246
|
// Settings → Privacy & security: the one switch, on this Mac only.
|
|
212
|
-
|
|
247
|
+
// 0.9.9: a setting kept only on one Mac (not in the synced config.json) wears a
|
|
248
|
+
// pill beside its label: "🖥️ studio only", with a styled tip. Synced settings
|
|
249
|
+
// get none, so the pill alone tells you which kind it is.
|
|
250
|
+
export const LOCAL_TIP = 'Kept on this Mac; doesn’t sync.'
|
|
251
|
+
export function localPillHtml({ machine, emoji = '', esc }) {
|
|
252
|
+
return ' <span class="local-pill" data-tip="' + esc(LOCAL_TIP) + '">' + (emoji ? '<span aria-hidden="true">' + esc(emoji) + '</span> ' : '') + esc(machine || 'this Mac') + ' only<span class="sr-only">: ' + esc(LOCAL_TIP) + '</span></span>'
|
|
253
|
+
}
|
|
254
|
+
// Away from that Mac the control is locked, and looks it: a lock, not just grey.
|
|
255
|
+
export function lockedAttrs(local) { return local ? '' : ' disabled aria-disabled="true"' }
|
|
256
|
+
export function lockHtml(local, icon) { return local ? '' : '<span class="lock" aria-hidden="true">' + (icon ? icon('lock') : '🔒') + '</span>' }
|
|
257
|
+
|
|
258
|
+
export function startsSettingHtml(info, { esc, local = true, emoji = '', icon = null } = {}) {
|
|
213
259
|
if (!info) return ''
|
|
214
|
-
|
|
215
|
-
|
|
260
|
+
const m = esc(info.machine || 'that Mac')
|
|
261
|
+
return '<label for="f-start-allow">Start sessions' + localPillHtml({ machine: info.machine, emoji, esc }) + '</label>' +
|
|
262
|
+
'<label class="check' + (local ? '' : ' locked') + '"><input id="f-start-allow" type="checkbox"' + (info.allowRemote ? ' checked' : '') + lockedAttrs(local) + '>' + lockHtml(local, icon) + '<span>Allow starting sessions from other devices</span></label>' +
|
|
263
|
+
'<div class="hint">Off: a session your phone or another Mac starts here waits until someone at this Mac confirms it. On: it starts right away, and this Mac shows a notification each time. Needs Trust my paired devices.</div>' +
|
|
264
|
+
'<div class="hint local-hint">' + (local ? 'Or run <code>leftoff starts on</code> on this Mac.' : 'Change on ' + m + ', or run <code>leftoff starts on</code> there.') + '</div>'
|
|
216
265
|
}
|