@leftoff/cli 0.9.8-beta.9 → 0.9.8
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 +39 -3
- package/bin/leftoff.js +405 -361
- package/docs/SECURITY.md +14 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/core.js +10 -0
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +441 -13
- package/ui/app.js +1082 -85
- package/ui/attach.js +211 -0
- package/ui/composer.js +46 -0
- package/ui/diff-view.js +278 -0
- package/ui/lightbox.js +452 -0
- package/ui/live-status.js +2 -1
- package/ui/live-view.js +22 -5
- package/ui/live-wake.js +96 -0
- package/ui/machines.js +17 -0
- package/ui/navhide.js +35 -0
- package/ui/pdfview.js +510 -0
- package/ui/prs.js +10 -5
- package/ui/relay-client.js +14 -4
- package/ui/restore-gate.js +20 -0
- package/ui/tabs.js +9 -5
- package/ui/tour.css +63 -0
- package/ui/tour.js +364 -0
- package/ui/update-bar.js +158 -0
- package/ui/vendor/PDFJS-LICENSE.txt +179 -0
- package/ui/vendor/pdfjs/pdf.min.js +27 -0
- package/ui/vendor/pdfjs/pdf.worker.min.js +27 -0
- package/ui/what-it-did.js +149 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// 0.9.8 "What it did" (docs/plans/see-what-it-did.md): a folded section on
|
|
2
|
+
// the session page with the last tools the session ran (and what they
|
|
3
|
+
// printed) and its folder's uncommitted changes. Nothing loads until you open
|
|
4
|
+
// it, nothing polls; ↻ reads it again. Pure HTML builders (state lives in
|
|
5
|
+
// app.js) so they can be checked without a browser.
|
|
6
|
+
import { icon } from './icons.js'
|
|
7
|
+
import { diffBlocks, splitPath } from './diff-view.js'
|
|
8
|
+
|
|
9
|
+
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
10
|
+
const plural = (n, w) => n + ' ' + w + (n === 1 ? '' : 's')
|
|
11
|
+
|
|
12
|
+
// The UI state for one session: { open, loading, tools, diff, toolsError,
|
|
13
|
+
// diffError, toolOpen: {id: true}, toolFull: {id: view}, toolLoading: {id},
|
|
14
|
+
// fileOpen: {path: true}, files: {path: view | {error}}, fileLoading: {path} }
|
|
15
|
+
export function widState() {
|
|
16
|
+
return { open: false, loading: false, tools: null, diff: null, toolsError: null, diffError: null, toolOpen: {}, toolFull: {}, toolLoading: {}, fileOpen: {}, files: {}, fileLoading: {} }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Short, plain words for what we can't show.
|
|
20
|
+
export function hiddenText(why) {
|
|
21
|
+
if (why === 'secret_file') return 'Contents hidden: a key or env file.'
|
|
22
|
+
if (why === 'binary') return 'Binary output, not shown.'
|
|
23
|
+
return 'Not shown.'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const STATUS = { modified: ['M', 'Modified'], added: ['A', 'Added'], deleted: ['D', 'Deleted'], untracked: ['U', 'New, not added to git yet'], typechange: ['T', 'Type changed'] }
|
|
27
|
+
|
|
28
|
+
function copyBtn(kind, key, label) {
|
|
29
|
+
return '<button type="button" class="copy-float" data-action="wid-copy" data-kind="' + kind + '" data-key="' + esc(key) + '" aria-label="' + esc(label) + '" data-tip="Copy">' + icon('copy') + '</button>'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toolRowHtml(t, ui, sid) {
|
|
33
|
+
const open = !!ui.toolOpen[t.id]
|
|
34
|
+
const full = ui.toolFull[t.id] || null
|
|
35
|
+
const v = full || t
|
|
36
|
+
const bodyId = 'wid-t-' + esc(t.id)
|
|
37
|
+
let h = '<li class="wid-item' + (t.error ? ' err' : '') + '">'
|
|
38
|
+
h += '<button type="button" class="wid-row" data-action="wid-tool" data-id="' + esc(sid) + '" data-tool="' + esc(t.id) + '" aria-expanded="' + open + '" aria-controls="' + bodyId + '">'
|
|
39
|
+
h += '<span class="wid-chev" aria-hidden="true">' + icon(open ? 'chevron-down' : 'chevron-right') + '</span>'
|
|
40
|
+
h += '<span class="wid-main"><span class="wid-label">' + esc(t.label) + (t.error ? ' <span class="wid-bad">failed</span>' : '') + (t.running ? ' <span class="wid-run">running</span>' : '') + '</span>'
|
|
41
|
+
if (t.what && !String(t.label || '').includes(t.what)) h += '<span class="wid-what mono">' + esc(t.what) + '</span>'
|
|
42
|
+
h += '</span></button>'
|
|
43
|
+
if (open) {
|
|
44
|
+
h += '<div class="wid-out" id="' + bodyId + '">'
|
|
45
|
+
if (v.running) h += '<p class="wid-note">Still running; no output yet.</p>'
|
|
46
|
+
else if (v.hidden) h += '<p class="wid-note">' + esc(hiddenText(v.hidden)) + '</p>'
|
|
47
|
+
else if (v.patch) h += '<div class="wid-pre-wrap gd-wrap"><div class="gd-slot" data-gd-path="' + esc('tool:' + t.id) + '"></div>' + copyBtn('tool', t.id, 'Copy the changes') + '</div>' // an edit: what it changed, drawn as a diff
|
|
48
|
+
else if (!v.output) h += '<p class="wid-note">No output.</p>'
|
|
49
|
+
else {
|
|
50
|
+
h += '<div class="wid-pre-wrap"><pre class="wid-pre"><code>' + esc(v.output) + '</code></pre>' + copyBtn('tool', t.id, 'Copy output') + '</div>'
|
|
51
|
+
if (v.clipped && !full) h += '<button type="button" class="btn wid-more" data-action="wid-tool-more" data-id="' + esc(sid) + '" data-tool="' + esc(t.id) + '"' + (ui.toolLoading[t.id] ? ' disabled' : '') + '>' + (ui.toolLoading[t.id] ? 'Loading…' : 'Show more') + '</button>'
|
|
52
|
+
else if (v.clipped) h += '<p class="wid-note">Cut to the first and last parts (' + esc(Math.round((v.chars || 0) / 1024)) + ' KB in all).</p>'
|
|
53
|
+
}
|
|
54
|
+
if (ui.toolLoading[t.id] === 'error') h += '<p class="wid-note">Couldn’t load more just now.</p>'
|
|
55
|
+
h += '</div>'
|
|
56
|
+
}
|
|
57
|
+
return h + '</li>'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function fileRowHtml(f, ui, sid) {
|
|
61
|
+
const open = !!ui.fileOpen[f.path]
|
|
62
|
+
const [letter, word] = STATUS[f.status] || ['M', 'Modified']
|
|
63
|
+
const bodyId = 'wid-f-' + esc(encodeURIComponent(f.path).replace(/%/g, '_'))
|
|
64
|
+
const bar = '<span class="gd-bar" aria-hidden="true">' + diffBlocks(f.add, f.del).map((b) => '<i class="gd-b' + b + '"></i>').join('') + '</span>'
|
|
65
|
+
const counts = f.binary ? '<span class="wid-n">binary</span>' : f.big ? '<span class="wid-n">big</span>' : '<span class="wid-add">+' + esc(f.add) + '</span> <span class="wid-del">−' + esc(f.del) + '</span>' + bar
|
|
66
|
+
let h = '<li class="wid-item' + (open ? ' open' : '') + '">'
|
|
67
|
+
h += '<button type="button" class="wid-row wid-file" data-action="wid-file" data-id="' + esc(sid) + '" data-path="' + esc(f.path) + '" aria-expanded="' + open + '" aria-controls="' + bodyId + '">'
|
|
68
|
+
h += '<span class="wid-chev" aria-hidden="true">' + icon(open ? 'chevron-down' : 'chevron-right') + '</span>'
|
|
69
|
+
h += '<span class="wid-st st-' + esc(f.status) + '" data-tip="' + esc(word) + '" aria-label="' + esc(word) + '">' + letter + '</span>'
|
|
70
|
+
const [dir, base] = splitPath(f.path)
|
|
71
|
+
h += '<span class="wid-path mono">' + (dir ? '<span class="wid-dir">' + esc(dir) + '</span>' : '') + '<b class="wid-base">' + esc(base) + '</b></span><span class="wid-counts">' + counts + '</span></button>'
|
|
72
|
+
if (open) {
|
|
73
|
+
const v = ui.files[f.path]
|
|
74
|
+
h += '<div class="wid-out" id="' + bodyId + '">'
|
|
75
|
+
if (f.hidden) h += '<p class="wid-note">Changed; contents hidden (a key or env file).</p>'
|
|
76
|
+
else if (f.binary) h += '<p class="wid-note">Binary file, not shown.</p>'
|
|
77
|
+
else if (f.big) h += '<p class="wid-note">Too big to show here.</p>'
|
|
78
|
+
else if (ui.fileLoading[f.path]) h += '<p class="wid-note">Loading…</p>'
|
|
79
|
+
else if (!v) h += ''
|
|
80
|
+
else if (v.error) h += '<p class="wid-note">' + esc(v.error) + '</p>'
|
|
81
|
+
else if (v.hidden) h += '<p class="wid-note">Changed; contents hidden (a key or env file).</p>'
|
|
82
|
+
else if (v.binary) h += '<p class="wid-note">Binary file, not shown.</p>'
|
|
83
|
+
else if (v.big) h += '<p class="wid-note">Too big to show here.</p>'
|
|
84
|
+
else if (!v.lines) h += '<p class="wid-note">No line changes (only its mode changed).</p>'
|
|
85
|
+
// the lines themselves are drawn as DOM by diff-view.js (hydrateDiffs), never as HTML
|
|
86
|
+
else h += '<div class="wid-pre-wrap gd-wrap"><div class="gd-slot" data-gd-path="' + esc(f.path) + '"></div>' + copyBtn('file', f.path, 'Copy the changes') + '</div>'
|
|
87
|
+
h += '</div>'
|
|
88
|
+
}
|
|
89
|
+
return h + '</li>'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function diffHtml(ui, sid) {
|
|
93
|
+
const d = ui.diff
|
|
94
|
+
let h = '<div class="wid-sub"><h3>This folder’s changes</h3>'
|
|
95
|
+
if (d && d.folder && !d.tooBig && !d.noRepo) h += '<span class="wid-meta">' + esc(d.folder) + (d.totals && d.totals.files ? ' · ' + plural(d.totals.files, 'file') + ' · <span class="wid-add">+' + esc(d.totals.add) + '</span> <span class="wid-del">−' + esc(d.totals.del) + '</span>' : '') + '</span>'
|
|
96
|
+
h += '</div>'
|
|
97
|
+
if (ui.diffError) return h + '<p class="wid-note">' + esc(ui.diffError) + '</p>'
|
|
98
|
+
if (!d) return h + '<p class="wid-note">Loading…</p>'
|
|
99
|
+
if (d.tooBig) return h + '<p class="wid-note">Too big to show here.</p>'
|
|
100
|
+
if (d.noRepo) return h + '<p class="wid-note">' + (d.noRepo === 'not_a_repo' ? 'This folder isn’t a git repository.' : d.noRepo === 'no_git' ? 'git isn’t installed on that Mac.' : 'This session’s folder isn’t there any more.') + '</p>'
|
|
101
|
+
if (!d.files || !d.files.length) return h + '<p class="wid-note">No uncommitted changes.</p>'
|
|
102
|
+
h += '<ul class="wid-list">' + d.files.map((f) => fileRowHtml(f, ui, sid)).join('') + '</ul>'
|
|
103
|
+
if (d.truncated) h += '<p class="wid-note">Showing the first ' + d.files.length + ' of ' + esc(d.totals.files) + ' files.</p>'
|
|
104
|
+
return h + '<p class="wid-foot">Uncommitted changes in the session’s folder (staged, unstaged and new files). Other sessions in this folder show the same changes.</p>'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toolsHtml(ui, sid) {
|
|
108
|
+
let h = '<div class="wid-sub"><h3>Last tools</h3></div>'
|
|
109
|
+
if (ui.toolsError) return h + '<p class="wid-note">' + esc(ui.toolsError) + '</p>'
|
|
110
|
+
if (!ui.tools) return h + '<p class="wid-note">Loading…</p>'
|
|
111
|
+
if (!ui.tools.length) return h + '<p class="wid-note">No tools run yet.</p>'
|
|
112
|
+
return h + '<ul class="wid-list">' + ui.tools.map((t) => toolRowHtml(t, ui, sid)).join('') + '</ul>'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The section. s: the session; ui: widState() for it.
|
|
116
|
+
export function whatItDidSection(s, ui = widState()) {
|
|
117
|
+
if (!s || s.agent) return ''
|
|
118
|
+
const sid = s.id
|
|
119
|
+
let h = '<div class="wid-head">'
|
|
120
|
+
h += '<button type="button" class="wid-toggle" data-action="wid-toggle" data-id="' + esc(sid) + '" aria-expanded="' + !!ui.open + '" aria-controls="wid-body">'
|
|
121
|
+
h += '<span class="wid-chev" aria-hidden="true">' + icon(ui.open ? 'chevron-down' : 'chevron-right') + '</span><h2>What it did</h2></button>'
|
|
122
|
+
if (ui.open) h += '<button type="button" class="icon wid-refresh" data-action="wid-refresh" data-id="' + esc(sid) + '" aria-label="Refresh" data-tip="Refresh"' + (ui.loading ? ' disabled' : '') + '>' + icon('refresh-cw') + '</button>'
|
|
123
|
+
h += '</div>'
|
|
124
|
+
if (!ui.open) return h
|
|
125
|
+
return h + '<div class="wid-body" id="wid-body">' + toolsHtml(ui, sid) + diffHtml(ui, sid) + '</div>'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The text a copy button copies.
|
|
129
|
+
export function copyText(ui, kind, key) {
|
|
130
|
+
if (kind === 'tool') { const v = ui.toolFull[key] || (ui.tools || []).find((t) => t.id === key); return v && v.patch ? v.patch : v && v.output ? v.output : '' }
|
|
131
|
+
if (kind === 'file') { const v = ui.files[key]; return v && v.lines ? v.lines : '' }
|
|
132
|
+
return ''
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// What a diff slot draws: a file's lines ("path") or an edit's patch ("tool:<id>").
|
|
136
|
+
export function slotSource(ui, key) {
|
|
137
|
+
const k = String(key || '')
|
|
138
|
+
if (k.startsWith('tool:')) { const id = k.slice(5); const v = ui.toolFull[id] || (ui.tools || []).find((t) => t.id === id); return v && v.patch ? v : null }
|
|
139
|
+
return ui.files[k] || null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A failed load in plain words (the other Mac offline says so itself).
|
|
143
|
+
export function loadError(e) {
|
|
144
|
+
const m = e && e.message ? String(e.message) : ''
|
|
145
|
+
if (e && e.status === 413) return 'Too big to show here.'
|
|
146
|
+
if (e && (e.status === 503 || /offline|didn’t answer/.test(m))) return m || 'That Mac isn’t answering right now.'
|
|
147
|
+
if (e && e.status === 404) return m || 'Not found.'
|
|
148
|
+
return m ? 'Couldn’t load: ' + m : 'Couldn’t load it just now.'
|
|
149
|
+
}
|