@gavin-hu/my-pi-agent 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -11
- package/README.zh-CN.md +9 -7
- package/extensions/bytefly/README.md +98 -116
- package/extensions/bytefly/notice/component.ts +6 -4
- package/extensions/bytefly/notice/config.ts +16 -2
- package/extensions/bytefly/panel/format.ts +3 -8
- package/extensions/bytefly/panel/join.ts +1 -6
- package/extensions/bytefly/panel/settings.ts +74 -377
- package/extensions/bytefly/panel/tui/entries.ts +16 -14
- package/extensions/bytefly/panel/tui/extensions-view.ts +3 -0
- package/extensions/bytefly/panel/tui/frame.ts +2 -2
- package/extensions/bytefly/panel/tui/settings-view.ts +8 -12
- package/extensions/bytefly/panel/tui.ts +4 -6
- package/extensions/bytefly/picker/resources.ts +7 -3
- package/extensions/bytefly/working/register.ts +2 -8
- package/extensions/check/tools.ts +2 -11
- package/extensions/context/README.md +162 -0
- package/extensions/context/collect.ts +53 -0
- package/extensions/context/commands.ts +100 -0
- package/extensions/context/format.ts +82 -0
- package/extensions/context/index.ts +16 -0
- package/extensions/context/report.ts +319 -0
- package/extensions/context/tui.ts +268 -0
- package/extensions/context/types.ts +122 -0
- package/extensions/file-browse/README.md +83 -8
- package/extensions/file-browse/client.ts +3 -1
- package/extensions/file-browse/config.ts +23 -0
- package/extensions/file-browse/files.ts +183 -48
- package/extensions/file-browse/fuzzy.ts +65 -11
- package/extensions/file-browse/html.ts +78 -133
- package/extensions/file-browse/index.ts +17 -2
- package/extensions/file-browse/live-client.ts +158 -0
- package/extensions/file-browse/live.ts +256 -0
- package/extensions/file-browse/markdown.ts +3 -10
- package/extensions/file-browse/raw-html.ts +3 -10
- package/extensions/file-browse/router.ts +33 -8
- package/extensions/file-browse/server.ts +78 -13
- package/extensions/file-browse/styles.ts +124 -0
- package/extensions/file-open/README.md +1 -2
- package/extensions/{document → file-read}/README.md +72 -56
- package/extensions/{document → file-read}/config.ts +12 -9
- package/extensions/{document → file-read}/extract/doc.ts +2 -2
- package/extensions/{document → file-read}/extract/docx.ts +3 -3
- package/extensions/{document → file-read}/extract/file.ts +9 -9
- package/extensions/{document → file-read}/extract/odt.ts +3 -3
- package/extensions/{document → file-read}/extract/pdf.ts +3 -3
- package/extensions/{document → file-read}/extract/rtf.ts +3 -3
- package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
- package/extensions/{document → file-read}/formats.ts +11 -11
- package/extensions/file-read/index.ts +17 -0
- package/extensions/{document → file-read}/paging.ts +15 -13
- package/extensions/{document → file-read}/render.ts +17 -13
- package/extensions/{document → file-read}/schema.ts +7 -7
- package/extensions/{document → file-read}/tool.ts +36 -28
- package/extensions/goal/tools.ts +10 -14
- package/extensions/job/commands.ts +32 -46
- package/extensions/job/runtime.ts +6 -8
- package/extensions/job/tools.ts +7 -26
- package/extensions/memory/commands.ts +30 -44
- package/extensions/memory/tools.ts +6 -14
- package/extensions/plan/tools/enter.ts +6 -7
- package/extensions/plan/tools/exit.ts +6 -7
- package/extensions/plan/tools/steps.ts +6 -7
- package/extensions/plan/tools/write.ts +8 -9
- package/extensions/plan/web-router.ts +2 -12
- package/extensions/plan/web.ts +30 -15
- package/extensions/question/tools.ts +5 -4
- package/extensions/rewind/checklist.ts +10 -3
- package/extensions/rewind/git.ts +27 -14
- package/extensions/rewind/hunk-picker.ts +1 -1
- package/extensions/rewind/path-picker.ts +1 -1
- package/extensions/rewind/review-tui.ts +3 -3
- package/extensions/rewind/snapshot.ts +13 -4
- package/extensions/subagent/background-tui.ts +1 -1
- package/extensions/subagent/background.ts +1 -7
- package/extensions/todo/tools.ts +10 -14
- package/extensions/watch/README.md +5 -5
- package/extensions/watch/commands.ts +33 -47
- package/extensions/watch/runtime.ts +5 -7
- package/extensions/watch/tools.ts +7 -18
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/search/search.ts +9 -18
- package/extensions/wechat/timer.ts +9 -30
- package/extensions/wechat/tools.ts +4 -7
- package/extensions/worktree/status.ts +14 -9
- package/lib/README.md +12 -8
- package/lib/escape.ts +15 -2
- package/lib/git/read.ts +20 -0
- package/lib/http/headers.ts +20 -0
- package/lib/http/server.ts +22 -4
- package/lib/pdf.ts +2 -2
- package/lib/pool.ts +62 -0
- package/lib/requirements.ts +2 -2
- package/lib/screen-session.ts +85 -0
- package/lib/timer.ts +61 -0
- package/lib/tui/README.md +3 -3
- package/lib/tui/columns.ts +33 -8
- package/lib/tui/dock-screen.ts +12 -4
- package/lib/tui/rows.ts +15 -6
- package/lib/tui/rules.ts +4 -8
- package/lib/tui/tool.ts +55 -2
- package/package.json +8 -4
- package/skills/changelog/SKILL.md +136 -0
- package/extensions/document/index.ts +0 -17
- /package/extensions/{document → file-read}/deadline.ts +0 -0
- /package/extensions/{document → file-read}/extract/vendor.d.ts +0 -0
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* parent directory. The matched character positions are returned so the client
|
|
9
9
|
* can highlight them; the palette never sees HTML.
|
|
10
10
|
*
|
|
11
|
+
* The palette runs this on every keystroke over an index of thousands of paths,
|
|
12
|
+
* so ranking keeps only the best `limit` candidates instead of sorting them all:
|
|
13
|
+
* the highlighted columns are computed for those winners and no others.
|
|
14
|
+
*
|
|
11
15
|
* Pure and dependency-free so it can be unit-tested without a DOM.
|
|
12
16
|
*/
|
|
13
17
|
|
|
@@ -24,15 +28,30 @@ export interface FuzzyResult {
|
|
|
24
28
|
/** Queries longer than this are treated as no match. */
|
|
25
29
|
const MAX_QUERY_LENGTH = 256;
|
|
26
30
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
/** A candidate while it is still unranked: a path and its score. */
|
|
32
|
+
interface Candidate {
|
|
33
|
+
path: string;
|
|
34
|
+
score: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A scan's result; `positions` is collected only when the caller asked for it. */
|
|
38
|
+
interface Scan {
|
|
39
|
+
score: number;
|
|
40
|
+
positions?: number[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The one subsequence scan. `collect` fills the matched columns, which the
|
|
45
|
+
* ranking pass does not need and allocates an array for on every candidate.
|
|
46
|
+
*/
|
|
47
|
+
function scan(query: string, text: string, collect: boolean): Scan | undefined {
|
|
48
|
+
if (query === "") return collect ? { score: 0, positions: [] } : { score: 0 };
|
|
30
49
|
if (query.length > MAX_QUERY_LENGTH) return undefined;
|
|
31
50
|
|
|
32
51
|
const lowerQuery = query.toLowerCase();
|
|
33
52
|
const lowerText = text.toLowerCase();
|
|
34
53
|
const baseStart = text.lastIndexOf("/") + 1;
|
|
35
|
-
const positions: number[] = [];
|
|
54
|
+
const positions: number[] | undefined = collect ? [] : undefined;
|
|
36
55
|
let score = 0;
|
|
37
56
|
let cursor = 0;
|
|
38
57
|
let previous = -2;
|
|
@@ -40,7 +59,7 @@ export function fuzzyMatch(query: string, text: string): FuzzyMatch | undefined
|
|
|
40
59
|
for (let index = 0; index < lowerQuery.length; index++) {
|
|
41
60
|
const found = lowerText.indexOf(lowerQuery[index], cursor);
|
|
42
61
|
if (found === -1) return undefined;
|
|
43
|
-
positions
|
|
62
|
+
positions?.push(found);
|
|
44
63
|
|
|
45
64
|
let bonus = 1;
|
|
46
65
|
if (text[found] === query[index]) bonus += 1;
|
|
@@ -55,21 +74,56 @@ export function fuzzyMatch(query: string, text: string): FuzzyMatch | undefined
|
|
|
55
74
|
previous = found;
|
|
56
75
|
cursor = found + 1;
|
|
57
76
|
}
|
|
58
|
-
return { score, positions };
|
|
77
|
+
return positions ? { score, positions } : { score };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Score one path against a query; `undefined` when it is not a subsequence. */
|
|
81
|
+
export function fuzzyMatch(query: string, text: string): FuzzyMatch | undefined {
|
|
82
|
+
const scanned = scan(query, text, true);
|
|
83
|
+
return scanned ? { score: scanned.score, positions: scanned.positions ?? [] } : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The palette's one total order: the higher score first, then the shorter path
|
|
88
|
+
* (a basename match beats a name buried in a directory), then the path itself so
|
|
89
|
+
* two candidates never compare equal.
|
|
90
|
+
*/
|
|
91
|
+
function compare(a: Candidate, b: Candidate): number {
|
|
92
|
+
if (a.score !== b.score) return b.score - a.score;
|
|
93
|
+
if (a.path.length !== b.path.length) return a.path.length - b.path.length;
|
|
94
|
+
return a.path.localeCompare(b.path);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Keep `candidate` in `window` — sorted by {@link compare}, never longer than `limit`. */
|
|
98
|
+
function keepBest(window: Candidate[], candidate: Candidate, limit: number): void {
|
|
99
|
+
let low = 0;
|
|
100
|
+
let high = window.length;
|
|
101
|
+
while (low < high) {
|
|
102
|
+
const middle = (low + high) >> 1;
|
|
103
|
+
if (compare(window[middle], candidate) <= 0) low = middle + 1;
|
|
104
|
+
else high = middle;
|
|
105
|
+
}
|
|
106
|
+
if (low >= limit) return;
|
|
107
|
+
window.splice(low, 0, candidate);
|
|
108
|
+
if (window.length > limit) window.pop();
|
|
59
109
|
}
|
|
60
110
|
|
|
61
111
|
/**
|
|
62
112
|
* Rank `paths` against `query` and return the best `limit` matches. An empty
|
|
63
113
|
* query returns nothing: the palette shows the index size instead.
|
|
114
|
+
*
|
|
115
|
+
* The result is the same list a full sort would produce — `keepBest` inserts on
|
|
116
|
+
* the same order — but the work is bounded by `limit` rather than by the index,
|
|
117
|
+
* and the matched columns are computed for the winners only.
|
|
64
118
|
*/
|
|
65
119
|
export function fuzzySearch(query: string, paths: readonly string[], limit: number): FuzzyResult[] {
|
|
66
120
|
const trimmed = query.trim();
|
|
67
121
|
if (trimmed === "" || limit <= 0) return [];
|
|
68
|
-
const
|
|
122
|
+
const window: Candidate[] = [];
|
|
69
123
|
for (const path of paths) {
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
124
|
+
const scored = scan(trimmed, path, false);
|
|
125
|
+
if (scored === undefined) continue;
|
|
126
|
+
keepBest(window, { path, score: scored.score }, limit);
|
|
72
127
|
}
|
|
73
|
-
|
|
74
|
-
return scored.slice(0, limit).map(({ path, positions }) => ({ path, positions }));
|
|
128
|
+
return window.map(({ path }) => ({ path, positions: fuzzyMatch(trimmed, path)?.positions ?? [] }));
|
|
75
129
|
}
|
|
@@ -4,30 +4,22 @@
|
|
|
4
4
|
* Pure string building: the router reads the filesystem and hands these
|
|
5
5
|
* renderers plain view models. Everything user-controlled is escaped, links are
|
|
6
6
|
* percent-encoded per segment, and the only script is the same-origin
|
|
7
|
-
* `/app.js`. The shared Bytefly shell and components come from `lib/web
|
|
8
|
-
* `browse-*` rules
|
|
7
|
+
* `/app.js`. The shared Bytefly shell and components come from `lib/web`, and
|
|
8
|
+
* the page-only `browse-*` rules from `styles.ts`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
12
|
-
import {
|
|
13
|
-
brandLockup,
|
|
14
|
-
docHead,
|
|
15
|
-
type KeyHint,
|
|
16
|
-
keysSheet,
|
|
17
|
-
SIDEBAR_COLLAPSE_MAX,
|
|
18
|
-
WEB_BASE_CSS,
|
|
19
|
-
WEB_COMPONENTS_CSS,
|
|
20
|
-
WEB_LAYOUT_CSS,
|
|
21
|
-
WEB_THEME_CSS,
|
|
22
|
-
} from "../../lib/web/index.ts";
|
|
12
|
+
import { brandLockup, docHead, type KeyHint, keysSheet } from "../../lib/web/index.ts";
|
|
23
13
|
import { escapeHtml } from "../../lib/escape.ts";
|
|
24
14
|
import { formatSize, type DirEntry, type FileView, type Listing, type TreeNode } from "./files.ts";
|
|
25
15
|
import { DIFF_MODES, statusLetter, type ChangedFile, type DiffLine, type DiffMode } from "./diff.ts";
|
|
26
16
|
import type { DiffFileView, GitStatus } from "./git.ts";
|
|
17
|
+
import type { ChangeIndex } from "./live.ts";
|
|
27
18
|
import { detectLanguage, highlightLines, type HighlightLanguage } from "../../lib/highlight.ts";
|
|
28
19
|
import { FOLDER_ICON, iconHref, languageOf, SPRITE, type IconSpec } from "./icons.ts";
|
|
29
20
|
import { renderMarkdown } from "./markdown.ts";
|
|
30
21
|
import { encodePath } from "./paths.ts";
|
|
22
|
+
import { BROWSE_CSS } from "./styles.ts";
|
|
31
23
|
|
|
32
24
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
33
25
|
|
|
@@ -51,17 +43,15 @@ function renderGit(git: GitStatus | undefined): string {
|
|
|
51
43
|
if (!name) return "";
|
|
52
44
|
const glyph = git.branch ? GLYPHS.branch : GLYPHS.detached;
|
|
53
45
|
const aria = git.branch ? `git branch ${git.branch}` : `git detached at ${git.head ?? ""}`;
|
|
54
|
-
return `<span class="bf-chip browse-git" title="${escapeHtml(aria)}" aria-label="${escapeHtml(aria)}"><span class="browse-git__glyph" aria-hidden="true">${escapeHtml(glyph)}</span>${escapeHtml(name)}</span>`;
|
|
46
|
+
return `<span class="bf-chip browse-git" title="${escapeHtml(aria)}" aria-label="${escapeHtml(aria)}"><span class="browse-git__glyph" aria-hidden="true">${escapeHtml(glyph)}</span><span class="browse-git__name">${escapeHtml(name)}</span></span>`;
|
|
55
47
|
}
|
|
56
48
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
dirs: ReadonlySet<string>;
|
|
64
|
-
}
|
|
49
|
+
/**
|
|
50
|
+
* The changed-path index for markers, plus the totals shown in the chip. It is
|
|
51
|
+
* the same index the live status feed sends, so the page and its updates agree
|
|
52
|
+
* on what "changed" means.
|
|
53
|
+
*/
|
|
54
|
+
export type ChangeSummary = ChangeIndex;
|
|
65
55
|
|
|
66
56
|
/** Whether a path carries a change marker: a changed file, or a directory above one. */
|
|
67
57
|
function isChanged(summary: ChangeSummary | undefined, rel: string, isDir: boolean): boolean {
|
|
@@ -76,122 +66,59 @@ function changeMark(summary: ChangeSummary | undefined, rel: string, isDir: bool
|
|
|
76
66
|
: "";
|
|
77
67
|
}
|
|
78
68
|
|
|
79
|
-
/** The
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
69
|
+
/** The live chrome a page renders: how often its stream is sent a frame. */
|
|
70
|
+
export interface LivePage {
|
|
71
|
+
intervalMs: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The header's connection chip. It starts hidden and says "connecting": the
|
|
76
|
+
* client reveals and paints it from the event stream, so a page with JS off — or
|
|
77
|
+
* with live status disabled — never shows a state it cannot keep true.
|
|
78
|
+
*
|
|
79
|
+
* It is deliberately not a `role="status"` region: the header is where a reader
|
|
80
|
+
* looks for the page's state, not a place to announce every change to it.
|
|
81
|
+
*/
|
|
82
|
+
function renderLiveChip(live: LivePage | undefined): string {
|
|
83
|
+
if (!live) return "";
|
|
84
|
+
return `<span class="bf-chip bf-chip--accent browse-live" id="live-state" title="Live status from the browse server" data-interval="${live.intervalMs}" hidden><span class="browse-live__glyph" aria-hidden="true">${escapeHtml(GLYPHS.browse)}</span><span class="browse-live__text">connecting</span></span>`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The footer's live line, hidden until the first frame: uptime and requests are
|
|
89
|
+
* server facts the rendered page does not know at build time.
|
|
90
|
+
*/
|
|
91
|
+
function renderLiveStatus(): string {
|
|
92
|
+
return `<span class="browse-live-line" id="live-status" hidden></span>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The header's changes chip, linking to the diff overview. With live status on,
|
|
97
|
+
* a hidden shell is always rendered so the chip can appear and disappear as the
|
|
98
|
+
* working tree changes, without the client inserting elements into the header.
|
|
99
|
+
*/
|
|
100
|
+
function renderChanges(summary: ChangeSummary | undefined, live: LivePage | undefined): string {
|
|
101
|
+
const count = summary?.count ?? 0;
|
|
102
|
+
if (count <= 0) {
|
|
103
|
+
return live
|
|
104
|
+
? `<a class="bf-chip browse-diff" id="browse-changes" href="/diff" title="View uncommitted changes" hidden></a>`
|
|
105
|
+
: "";
|
|
106
|
+
}
|
|
107
|
+
const label = `${count} change${count === 1 ? "" : "s"}`;
|
|
108
|
+
const insertions = summary?.insertions ?? 0;
|
|
109
|
+
const deletions = summary?.deletions ?? 0;
|
|
83
110
|
const totals =
|
|
84
|
-
|
|
85
|
-
? ` <span class="browse-diff-totals"><span class="browse-diff-add">+${
|
|
111
|
+
insertions + deletions > 0
|
|
112
|
+
? ` <span class="browse-diff-totals"><span class="browse-diff-add">+${insertions}</span> <span class="browse-diff-del">−${deletions}</span></span>`
|
|
86
113
|
: "";
|
|
87
|
-
const aria = `${label} against HEAD${
|
|
88
|
-
return `<a class="bf-chip browse-diff" href="/diff" title="View uncommitted changes" aria-label="${escapeHtml(aria)}"><span class="browse-diff__dot" aria-hidden="true">●</span>${escapeHtml(label)}${totals}</a>`;
|
|
114
|
+
const aria = `${label} against HEAD${insertions + deletions > 0 ? `, ${insertions} insertions and ${deletions} deletions` : ""}`;
|
|
115
|
+
return `<a class="bf-chip browse-diff" id="browse-changes" href="/diff" title="View uncommitted changes" aria-label="${escapeHtml(aria)}"><span class="browse-diff__dot" aria-hidden="true">●</span>${escapeHtml(label)}${totals}</a>`;
|
|
89
116
|
}
|
|
90
117
|
|
|
91
118
|
function hrefFor(entry: { rel: string; isDir: boolean }): string {
|
|
92
119
|
return entry.isDir ? `/browse/${encodePath(entry.rel)}` : `/view/${encodePath(entry.rel)}`;
|
|
93
120
|
}
|
|
94
121
|
|
|
95
|
-
/** The shared shell, components, and the page-only `browse-*` rules. */
|
|
96
|
-
const STYLES = `${WEB_BASE_CSS}
|
|
97
|
-
${WEB_THEME_CSS}
|
|
98
|
-
${WEB_LAYOUT_CSS}
|
|
99
|
-
${WEB_COMPONENTS_CSS}
|
|
100
|
-
.browse-side{padding:var(--space-3)}
|
|
101
|
-
.browse-root{flex:1 1 auto;direction:rtl;text-align:left}
|
|
102
|
-
.browse-git{font-family:var(--font-mono)}
|
|
103
|
-
.browse-quick-open{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);width:100%;margin-bottom:var(--space-2);padding:6px var(--space-2);border:1px solid var(--border);border-radius:var(--radius-md);background:var(--canvas);color:var(--muted);font:var(--fs-sm) var(--font-ui);cursor:pointer}
|
|
104
|
-
.browse-quick-open:hover{border-color:var(--accent);color:var(--accent)}
|
|
105
|
-
.browse-quick-open kbd{font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--dim)}
|
|
106
|
-
.browse-side__filters{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2)}
|
|
107
|
-
.browse-side__filters .bf-field{flex:1 1 auto;min-width:0}
|
|
108
|
-
.browse-side__filters .bf-btn{flex:0 0 auto;white-space:nowrap}
|
|
109
|
-
.browse-cat{width:16px;height:16px;flex:0 0 16px;color:var(--accent)}
|
|
110
|
-
.browse-cat.doc{color:var(--muted)}.browse-cat.img{color:var(--success)}.browse-cat.cfg{color:var(--warn)}.browse-cat.arc{color:var(--warn)}
|
|
111
|
-
.browse-heading{margin:0 0 var(--space-3);font-size:var(--fs-lg)}
|
|
112
|
-
.browse-listing th.bf-table__size,.browse-listing th.bf-table__when{text-align:right}
|
|
113
|
-
.browse-listing th.bf-table__size{width:96px}
|
|
114
|
-
.browse-listing th.bf-table__when{width:140px}
|
|
115
|
-
.browse-dotfile .bf-table__name a{opacity:.72}
|
|
116
|
-
.browse-broken{color:var(--muted);text-decoration:line-through}
|
|
117
|
-
.browse-link-mark{color:var(--muted);font-size:var(--fs-xs)}
|
|
118
|
-
.browse-when{display:none}
|
|
119
|
-
body.palette-open{overflow:hidden}
|
|
120
|
-
.browse-diff{font-family:var(--font-mono);gap:5px}
|
|
121
|
-
.browse-diff:hover{color:var(--accent);text-decoration:none}
|
|
122
|
-
.browse-diff__dot{color:var(--warn);font-size:9px;line-height:1}
|
|
123
|
-
.browse-change{color:var(--warn);font-size:9px;line-height:1;flex:0 0 auto}
|
|
124
|
-
.browse-diff-totals{font-family:var(--font-mono)}
|
|
125
|
-
.browse-diff-add{color:var(--success)}
|
|
126
|
-
.browse-diff-del{color:var(--danger)}
|
|
127
|
-
.browse-diff-scope{color:var(--muted);font-size:var(--fs-sm);font-weight:var(--fw-medium)}
|
|
128
|
-
.browse-diff-toolbar{display:flex;align-items:center;gap:var(--space-3);margin:0 0 var(--space-3);flex-wrap:wrap}
|
|
129
|
-
.browse-diff-stat{margin:0 0 var(--space-3);font:var(--fs-xs)/1.5 var(--font-mono);color:var(--muted);white-space:pre-wrap;overflow-x:auto}
|
|
130
|
-
.browse-diff th.browse-diff-status{width:72px}
|
|
131
|
-
.browse-diff th.browse-diff-word{width:130px}
|
|
132
|
-
.browse-diff-badge{display:inline-block;min-width:16px;text-align:center;font:var(--fw-semibold) var(--fs-xs) var(--font-mono);border-radius:var(--radius-sm);padding:1px 5px;border:1px solid var(--border)}
|
|
133
|
-
.browse-diff-badge--M{color:var(--warn);border-color:currentColor}
|
|
134
|
-
.browse-diff-badge--A,.browse-diff-badge--Q{color:var(--success);border-color:currentColor}
|
|
135
|
-
.browse-diff-badge--D{color:var(--danger);border-color:currentColor}
|
|
136
|
-
.browse-diff-badge--R,.browse-diff-badge--C{color:var(--accent);border-color:currentColor}
|
|
137
|
-
.browse-diff-orig{color:var(--muted);font-size:var(--fs-xs)}
|
|
138
|
-
.browse-diff-word{color:var(--muted);font-size:var(--fs-sm)}
|
|
139
|
-
.browse-diff-no{display:inline-block;width:46px;padding-right:var(--space-2);text-align:right;color:var(--dim);user-select:none}
|
|
140
|
-
.browse-diff-sign{display:inline-block;width:16px;text-align:center;color:var(--muted);user-select:none}
|
|
141
|
-
.bf-diff-line--add{background:var(--success-soft)}
|
|
142
|
-
.bf-diff-line--add .browse-diff-sign{color:var(--success)}
|
|
143
|
-
.bf-diff-line--del{background:var(--danger-soft)}
|
|
144
|
-
.bf-diff-line--del .browse-diff-sign{color:var(--danger)}
|
|
145
|
-
.bf-diff-line--hunk{color:var(--accent);background:var(--canvas)}
|
|
146
|
-
.bf-diff-line--meta{color:var(--muted)}
|
|
147
|
-
.bf-diff-line--note{color:var(--muted);font-style:italic}
|
|
148
|
-
/* A hunk header pins to the top of the diff viewport while its body scrolls.
|
|
149
|
-
Each hunk is a section so the pinned header is bounded by its own body and
|
|
150
|
-
cannot stack on the next one. */
|
|
151
|
-
.bf-diff-hunk{display:block}
|
|
152
|
-
.bf-diff .bf-diff-line--hunk{position:sticky;top:0;z-index:1}
|
|
153
|
-
/* Reading progress, pinned to the sticky header's bottom edge. */
|
|
154
|
-
.browse-progress{position:absolute;left:0;right:0;bottom:-1px;height:2px;overflow:hidden}
|
|
155
|
-
.browse-progress__fill{display:block;height:100%;width:0;background:var(--accent);transition:width var(--motion-fast) linear}
|
|
156
|
-
/* File and diff pages: a measured reading column inside the card. */
|
|
157
|
-
/* The Markdown body fills the card, so the scroll box is the card's width: its
|
|
158
|
-
scrollbar sits at the card edge. The measure lives on the blocks themselves:
|
|
159
|
-
on .bf-md it would be a flex item with auto inline margins, which is
|
|
160
|
-
shrink-to-fit, so a short document collapsed to its widest line. */
|
|
161
|
-
.browse-measure{--browse-measure:1012px}
|
|
162
|
-
.browse-measure .bf-md{--browse-gutter:max(0px,calc((100% - var(--browse-measure)) / 2));max-width:none;margin-inline:0;padding-inline:var(--space-5);padding-block:var(--space-4) var(--space-6)}
|
|
163
|
-
.browse-measure .bf-md>*{max-width:var(--browse-measure);margin-inline:auto}
|
|
164
|
-
/* Tables start at the prose column and take only the gutter to its right, so a
|
|
165
|
-
wide window widens them without a dead band on their left. Code stays inside
|
|
166
|
-
the measure with the rest of the prose. */
|
|
167
|
-
.browse-measure .bf-md>.md-table-frame{max-width:none;padding-left:var(--browse-gutter)}
|
|
168
|
-
/* A table wider than the pane scrolls, but an overlay scrollbar is invisible
|
|
169
|
-
until it moves; a gradient on the frame marks the columns still out of view.
|
|
170
|
-
It has to be the frame: the scroller clips its own content, so a pseudo-element
|
|
171
|
-
inside it would scroll away with the table. */
|
|
172
|
-
.browse-measure .bf-md>.md-table-frame.browse-table-more::after{content:"";position:absolute;top:0;right:0;bottom:0;width:40px;pointer-events:none;background:linear-gradient(to left,var(--surface) 12%,transparent)}
|
|
173
|
-
.bf-card__meta{font-variant-numeric:tabular-nums}
|
|
174
|
-
.browse-diff-summary{font-family:var(--font-mono);white-space:nowrap}
|
|
175
|
-
.browse-diff-summary .browse-diff-add{color:var(--success)}
|
|
176
|
-
.browse-diff-summary .browse-diff-del{color:var(--danger)}
|
|
177
|
-
.browse-hunks{color:var(--muted)}
|
|
178
|
-
/* Wide screens: pin quick open and the filter, and scroll the tree on its own
|
|
179
|
-
box. The sidebar is a details element, whose content sits in an anonymous
|
|
180
|
-
wrapper with no definite height, so the panel fills the sidebar by absolute
|
|
181
|
-
positioning; below the sidebar breakpoint the disclosure keeps normal page
|
|
182
|
-
scroll. */
|
|
183
|
-
@media not (max-width:${SIDEBAR_COLLAPSE_MAX}px){
|
|
184
|
-
.browse-sidebar{position:relative;overflow:hidden}
|
|
185
|
-
.browse-side{position:absolute;inset:0;display:flex;flex-direction:column;overflow:hidden}
|
|
186
|
-
.browse-side .bf-tree{flex:1 1 auto;min-height:0;overflow:auto}
|
|
187
|
-
}
|
|
188
|
-
@media (max-width:820px){
|
|
189
|
-
.browse-root{order:9;flex:1 1 100%;direction:ltr;text-align:left}
|
|
190
|
-
}
|
|
191
|
-
@media (max-width:520px){
|
|
192
|
-
.bf-toolbar .bf-theme-toggle{margin-left:auto}
|
|
193
|
-
}`;
|
|
194
|
-
|
|
195
122
|
interface Layout {
|
|
196
123
|
title: string;
|
|
197
124
|
rootLabel: string;
|
|
@@ -207,6 +134,8 @@ interface Layout {
|
|
|
207
134
|
git?: GitStatus;
|
|
208
135
|
/** Changed-path index for the header chip and the tree markers; omitted when clean. */
|
|
209
136
|
summary?: ChangeSummary;
|
|
137
|
+
/** Live status chrome; omitted when the channel is off. */
|
|
138
|
+
live?: LivePage;
|
|
210
139
|
}
|
|
211
140
|
|
|
212
141
|
/** A full two-pane page. */
|
|
@@ -214,7 +143,7 @@ function renderLayout(layout: Layout): string {
|
|
|
214
143
|
return `<!doctype html>
|
|
215
144
|
<html lang="en">
|
|
216
145
|
<head>
|
|
217
|
-
${docHead({ title: layout.title, css:
|
|
146
|
+
${docHead({ title: layout.title, css: BROWSE_CSS })}
|
|
218
147
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' fill='%2358a6ff'/%3E%3C/svg%3E">
|
|
219
148
|
</head>
|
|
220
149
|
<body class="bf-app">
|
|
@@ -226,8 +155,9 @@ ${SPRITE}
|
|
|
226
155
|
${brandLockup("browse")}
|
|
227
156
|
<span class="bf-context browse-root">${escapeHtml(layout.rootLabel)}</span>
|
|
228
157
|
${renderGit(layout.git)}
|
|
229
|
-
${renderChanges(layout.summary)}
|
|
158
|
+
${renderChanges(layout.summary, layout.live)}
|
|
230
159
|
<span class="bf-chip bf-chip--accent">read-only</span>
|
|
160
|
+
${renderLiveChip(layout.live)}
|
|
231
161
|
<button type="button" class="bf-btn bf-btn--icon browse-keys-btn" id="keys-open" aria-label="Keyboard shortcuts" title="Keyboard shortcuts">?</button>
|
|
232
162
|
<button type="button" class="bf-btn bf-btn--icon bf-theme-toggle" id="theme-toggle" aria-label="Toggle light and dark theme" title="Toggle light and dark theme"><span class="theme-glyph" aria-hidden="true">☾</span></button>
|
|
233
163
|
</div><div class="browse-progress" aria-hidden="true"><span class="browse-progress__fill" id="progress-fill"></span></div></header>
|
|
@@ -249,7 +179,7 @@ ${SPRITE}
|
|
|
249
179
|
${layout.main}
|
|
250
180
|
</main>
|
|
251
181
|
</div>
|
|
252
|
-
<footer class="bf-app__footer"
|
|
182
|
+
<footer class="bf-app__footer">${layout.live ? renderLiveStatus() : ""}served from ${escapeHtml(layout.rootLabel)} · read-only · ${escapeHtml(layout.footerNote)}</footer>
|
|
253
183
|
${layout.keys && layout.keys.length > 0 ? keysSheet(layout.keys) : ""}
|
|
254
184
|
<script src="/app.js" defer></script>
|
|
255
185
|
</body>
|
|
@@ -326,6 +256,8 @@ export interface DirectoryPage {
|
|
|
326
256
|
git?: GitStatus;
|
|
327
257
|
/** Changed-path index for the header chip and markers. */
|
|
328
258
|
summary?: ChangeSummary;
|
|
259
|
+
/** Live status chrome; omitted when the channel is off. */
|
|
260
|
+
live?: LivePage;
|
|
329
261
|
}
|
|
330
262
|
|
|
331
263
|
/** Render a directory listing page. */
|
|
@@ -354,6 +286,7 @@ ${page.listing.entries.map((entry) => renderRow(entry, page.maxThumbBytes, page.
|
|
|
354
286
|
footerNote: `${page.listing.total} entr${page.listing.total === 1 ? "y" : "ies"}`,
|
|
355
287
|
git: page.git,
|
|
356
288
|
summary: page.summary,
|
|
289
|
+
live: page.live,
|
|
357
290
|
});
|
|
358
291
|
}
|
|
359
292
|
|
|
@@ -372,6 +305,8 @@ export interface FilePage {
|
|
|
372
305
|
changed?: boolean;
|
|
373
306
|
/** Changed-path index for the header chip and the tree markers. */
|
|
374
307
|
summary?: ChangeSummary;
|
|
308
|
+
/** Live status chrome; omitted when the channel is off. */
|
|
309
|
+
live?: LivePage;
|
|
375
310
|
}
|
|
376
311
|
|
|
377
312
|
/** The root-relative directory of a file, for resolving its Markdown links. */
|
|
@@ -490,6 +425,7 @@ export function renderFilePage(page: FilePage): string {
|
|
|
490
425
|
footerNote: page.rel,
|
|
491
426
|
git: page.git,
|
|
492
427
|
summary: page.summary,
|
|
428
|
+
live: page.live,
|
|
493
429
|
});
|
|
494
430
|
}
|
|
495
431
|
|
|
@@ -614,6 +550,8 @@ export interface DiffPage {
|
|
|
614
550
|
tree: TreeNode;
|
|
615
551
|
git?: GitStatus;
|
|
616
552
|
summary?: ChangeSummary;
|
|
553
|
+
/** Live status chrome; omitted when the channel is off. */
|
|
554
|
+
live?: LivePage;
|
|
617
555
|
}
|
|
618
556
|
|
|
619
557
|
/** The change overview: `git diff --stat` plus every changed path. */
|
|
@@ -641,6 +579,7 @@ ${page.files.map((file) => renderChangedRow(file, page.mode)).join("\n")}
|
|
|
641
579
|
footerNote: `${page.files.length} changed`,
|
|
642
580
|
git: page.git,
|
|
643
581
|
summary: page.summary,
|
|
582
|
+
live: page.live,
|
|
644
583
|
});
|
|
645
584
|
}
|
|
646
585
|
|
|
@@ -661,6 +600,8 @@ export interface FileDiffPage {
|
|
|
661
600
|
tree: TreeNode;
|
|
662
601
|
git?: GitStatus;
|
|
663
602
|
summary?: ChangeSummary;
|
|
603
|
+
/** Live status chrome; omitted when the channel is off. */
|
|
604
|
+
live?: LivePage;
|
|
664
605
|
}
|
|
665
606
|
|
|
666
607
|
/** A single file's unified diff, with the scope control and file actions. */
|
|
@@ -710,6 +651,7 @@ export function renderFileDiffPage(page: FileDiffPage): string {
|
|
|
710
651
|
footerNote: `diff · ${modeScope(page.mode)}`,
|
|
711
652
|
git: page.git,
|
|
712
653
|
summary: page.summary,
|
|
654
|
+
live: page.live,
|
|
713
655
|
});
|
|
714
656
|
}
|
|
715
657
|
|
|
@@ -719,6 +661,8 @@ export interface ErrorPage {
|
|
|
719
661
|
message: string;
|
|
720
662
|
tree: TreeNode;
|
|
721
663
|
git?: GitStatus;
|
|
664
|
+
/** Live status chrome; omitted when the channel is off. */
|
|
665
|
+
live?: LivePage;
|
|
722
666
|
}
|
|
723
667
|
|
|
724
668
|
/** Render a status/error page with the same chrome. */
|
|
@@ -732,5 +676,6 @@ export function renderErrorPage(page: ErrorPage): string {
|
|
|
732
676
|
main: ` <div class="bf-empty"><div class="bf-empty__code">${page.status}</div><p class="bf-empty__title">${escapeHtml(page.message)}</p><p class="bf-empty__sub"><a href="/browse/">back to the served folder</a></p></div>`,
|
|
733
677
|
footerNote: "error",
|
|
734
678
|
git: page.git,
|
|
679
|
+
live: page.live,
|
|
735
680
|
});
|
|
736
681
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Registers the read-only `/browse` command. It starts a `node:http` server
|
|
5
5
|
* bound to 127.0.0.1, rooted at the effective cwd, and opens the default
|
|
6
6
|
* browser at a two-pane tree browser. No tool is registered; the server is
|
|
7
|
-
* closed on `session_shutdown
|
|
7
|
+
* closed on `session_shutdown`, by `/browse stop`, and once its last page has
|
|
8
|
+
* been gone for the idle grace — a closed window is not a running server.
|
|
8
9
|
*
|
|
9
10
|
* Load with: pi --extension ./extensions/file-browse
|
|
10
11
|
*/
|
|
@@ -32,6 +33,8 @@ export interface FileBrowserDeps {
|
|
|
32
33
|
|
|
33
34
|
interface RunningServer {
|
|
34
35
|
server: FileServer;
|
|
36
|
+
/** The context of the command that started it, used to clear the chip later. */
|
|
37
|
+
ctx: ExtensionContext;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export default function fileBrowser(pi: ExtensionAPI, deps: FileBrowserDeps = {}): void {
|
|
@@ -62,6 +65,17 @@ export default function fileBrowser(pi: ExtensionAPI, deps: FileBrowserDeps = {}
|
|
|
62
65
|
return true;
|
|
63
66
|
};
|
|
64
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Close the server once the page that was its only reader has gone: nothing is
|
|
70
|
+
* left to serve, and the chip would otherwise advertise a port that answers no
|
|
71
|
+
* one. `/browse` starts it again on the same session port.
|
|
72
|
+
*/
|
|
73
|
+
const stopWhenPageLeft = async (): Promise<void> => {
|
|
74
|
+
const current = running;
|
|
75
|
+
if (!current) return;
|
|
76
|
+
if (await stop(current.ctx)) current.ctx.ui.notify("browse: stopped — the last page was closed", "info");
|
|
77
|
+
};
|
|
78
|
+
|
|
65
79
|
const start = async (args: string, ctx: ExtensionContext): Promise<void> => {
|
|
66
80
|
const target = args.trim();
|
|
67
81
|
if (target === "status") {
|
|
@@ -121,9 +135,10 @@ export default function fileBrowser(pi: ExtensionAPI, deps: FileBrowserDeps = {}
|
|
|
121
135
|
maxFileBytes: config.maxFileBytes,
|
|
122
136
|
maxTextLines: config.maxTextLines,
|
|
123
137
|
}),
|
|
138
|
+
onIdle: () => void stopWhenPageLeft(),
|
|
124
139
|
});
|
|
125
140
|
if (unpinned) sessionPort = port;
|
|
126
|
-
running = { server };
|
|
141
|
+
running = { server, ctx };
|
|
127
142
|
setBrowseStatus(ctx, server.port);
|
|
128
143
|
ctx.ui.notify(`browse: ${server.root} at ${server.url}`, "info");
|
|
129
144
|
if (config.autoOpen) (deps.open ?? openInBrowser)(server.url);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live status block of the browse client script.
|
|
3
|
+
*
|
|
4
|
+
* It is a fragment of the same IIFE `client.ts` ships (`LIVE_CLIENT_JS` is
|
|
5
|
+
* interpolated into `CLIENT_JS`), kept in its own module so the client script
|
|
6
|
+
* stays one readable unit per concern. Everything it paints is text it was
|
|
7
|
+
* handed — a branch name, a path, a count — so a repository cannot inject markup
|
|
8
|
+
* into the page through a status frame.
|
|
9
|
+
*
|
|
10
|
+
* It does nothing at all unless the server rendered the connection chip, which
|
|
11
|
+
* is what makes `live: false` and JS-off leave the page exactly as it was.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const LIVE_CLIENT_JS = ` // Live status: one event stream feeds the header chips, the tree and listing
|
|
15
|
+
// markers, and the footer line. The server owns the cadence; this only paints.
|
|
16
|
+
var liveChip = document.getElementById("live-state");
|
|
17
|
+
if (liveChip) {
|
|
18
|
+
var liveText = liveChip.querySelector(".browse-live__text");
|
|
19
|
+
var liveLine = document.getElementById("live-status");
|
|
20
|
+
var liveGit = document.querySelector(".browse-git");
|
|
21
|
+
var liveChanges = document.getElementById("browse-changes");
|
|
22
|
+
var liveInterval = Number(liveChip.getAttribute("data-interval")) || 3000;
|
|
23
|
+
var liveStale = 0;
|
|
24
|
+
var liveTone = "";
|
|
25
|
+
|
|
26
|
+
function formatSpan(ms) {
|
|
27
|
+
var seconds = Math.max(0, Math.round(ms / 1000));
|
|
28
|
+
if (seconds < 60) return seconds + "s";
|
|
29
|
+
var minutes = Math.floor(seconds / 60);
|
|
30
|
+
if (minutes < 60) return minutes + "m " + String(seconds % 60).padStart(2, "0") + "s";
|
|
31
|
+
return Math.floor(minutes / 60) + "h " + String(minutes % 60).padStart(2, "0") + "m";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setLiveTone(tone, label) {
|
|
35
|
+
liveTone = tone;
|
|
36
|
+
liveChip.className = "bf-chip browse-live" + (tone ? " bf-chip--" + tone : "");
|
|
37
|
+
liveChip.hidden = false;
|
|
38
|
+
if (liveText) liveText.textContent = label;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The chip names its state and never counts: nothing is repainted between
|
|
42
|
+
// frames, and the age of the last one is the footer's uptime to report. Three
|
|
43
|
+
// missed frames on an open connection is a stalled feed, not a live one, so
|
|
44
|
+
// one timeout is armed per frame instead of a second-by-second tick.
|
|
45
|
+
function armLiveStale() {
|
|
46
|
+
window.clearTimeout(liveStale);
|
|
47
|
+
liveStale = window.setTimeout(function () {
|
|
48
|
+
if (liveTone === "success") setLiveTone("warn", "stale");
|
|
49
|
+
}, liveInterval * 3);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function paintLiveGit(git) {
|
|
53
|
+
if (!liveGit || !git) return;
|
|
54
|
+
var name = git.branch || git.head || "";
|
|
55
|
+
if (!name) return;
|
|
56
|
+
var aria = git.branch ? "git branch " + git.branch : "git detached at " + git.head;
|
|
57
|
+
liveGit.title = aria;
|
|
58
|
+
liveGit.setAttribute("aria-label", aria);
|
|
59
|
+
var glyph = liveGit.querySelector(".browse-git__glyph");
|
|
60
|
+
if (glyph) glyph.textContent = git.branch ? "\\u2387" : "\\u26A0";
|
|
61
|
+
var label = liveGit.querySelector(".browse-git__name");
|
|
62
|
+
if (label) label.textContent = name;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function paintLiveMarks(changes) {
|
|
66
|
+
var files = {};
|
|
67
|
+
var dirs = {};
|
|
68
|
+
((changes && changes.files) || []).forEach(function (path) { files[path] = true; });
|
|
69
|
+
((changes && changes.dirs) || []).forEach(function (path) { dirs[path] = true; });
|
|
70
|
+
var rows = document.querySelectorAll(".browse-listing tbody tr[data-path], nav.bf-tree .bf-tree__row[data-path]");
|
|
71
|
+
Array.prototype.forEach.call(rows, function (row) {
|
|
72
|
+
var path = row.getAttribute("data-path");
|
|
73
|
+
var inTree = row.parentElement && row.parentElement.tagName === "LI";
|
|
74
|
+
var isDir = inTree ? !!row.querySelector(".bf-tree__tw--toggle") : row.getAttribute("data-dir") === "1";
|
|
75
|
+
var marked = isDir ? dirs[path] === true : files[path] === true;
|
|
76
|
+
var mark = row.querySelector(".browse-change");
|
|
77
|
+
if (marked && !mark) {
|
|
78
|
+
mark = document.createElement("span");
|
|
79
|
+
mark.className = "browse-change";
|
|
80
|
+
mark.title = "uncommitted changes";
|
|
81
|
+
mark.setAttribute("aria-label", "uncommitted changes");
|
|
82
|
+
mark.textContent = "\\u25CF";
|
|
83
|
+
var name = row.querySelector(".bf-table__name");
|
|
84
|
+
(name || row).appendChild(mark);
|
|
85
|
+
} else if (!marked && mark && mark.parentNode) {
|
|
86
|
+
mark.parentNode.removeChild(mark);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function paintLiveChanges(changes) {
|
|
92
|
+
var count = changes ? changes.count : 0;
|
|
93
|
+
if (liveChanges) {
|
|
94
|
+
liveChanges.hidden = count <= 0;
|
|
95
|
+
if (count > 0) {
|
|
96
|
+
var label = count + (count === 1 ? " change" : " changes");
|
|
97
|
+
var totals = (changes.insertions || 0) + (changes.deletions || 0);
|
|
98
|
+
liveChanges.textContent = "";
|
|
99
|
+
var dot = document.createElement("span");
|
|
100
|
+
dot.className = "browse-diff__dot";
|
|
101
|
+
dot.setAttribute("aria-hidden", "true");
|
|
102
|
+
dot.textContent = "\\u25CF";
|
|
103
|
+
liveChanges.appendChild(dot);
|
|
104
|
+
liveChanges.appendChild(document.createTextNode(label));
|
|
105
|
+
if (totals > 0) {
|
|
106
|
+
var sum = document.createElement("span");
|
|
107
|
+
sum.className = "browse-diff-totals";
|
|
108
|
+
var add = document.createElement("span");
|
|
109
|
+
add.className = "browse-diff-add";
|
|
110
|
+
add.textContent = "+" + changes.insertions;
|
|
111
|
+
var del = document.createElement("span");
|
|
112
|
+
del.className = "browse-diff-del";
|
|
113
|
+
del.textContent = "\\u2212" + changes.deletions;
|
|
114
|
+
sum.appendChild(add);
|
|
115
|
+
sum.appendChild(document.createTextNode(" "));
|
|
116
|
+
sum.appendChild(del);
|
|
117
|
+
liveChanges.appendChild(sum);
|
|
118
|
+
}
|
|
119
|
+
var aria = label + " against HEAD";
|
|
120
|
+
if (totals > 0) aria += ", " + changes.insertions + " insertions and " + changes.deletions + " deletions";
|
|
121
|
+
liveChanges.setAttribute("aria-label", aria);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
paintLiveMarks(changes);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function paintLiveSnapshot(snapshot) {
|
|
128
|
+
setLiveTone("success", "live");
|
|
129
|
+
armLiveStale();
|
|
130
|
+
if (liveLine) {
|
|
131
|
+
liveLine.hidden = false;
|
|
132
|
+
var requests = Number(snapshot.requests) || 0;
|
|
133
|
+
liveLine.textContent = "up " + formatSpan(Number(snapshot.uptimeMs) || 0) + " \\u00B7 " + requests + (requests === 1 ? " request" : " requests");
|
|
134
|
+
}
|
|
135
|
+
paintLiveGit(snapshot.git);
|
|
136
|
+
paintLiveChanges(snapshot.changes);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (window.EventSource) {
|
|
140
|
+
var liveStream = new EventSource("/events");
|
|
141
|
+
liveStream.addEventListener("open", function () {
|
|
142
|
+
if (liveTone !== "success") setLiveTone("accent", "connecting");
|
|
143
|
+
});
|
|
144
|
+
liveStream.addEventListener("status", function (event) {
|
|
145
|
+
var snapshot;
|
|
146
|
+
try { snapshot = JSON.parse(event.data); } catch (error) { return; }
|
|
147
|
+
paintLiveSnapshot(snapshot);
|
|
148
|
+
});
|
|
149
|
+
liveStream.addEventListener("error", function () {
|
|
150
|
+
setLiveTone("danger", "offline");
|
|
151
|
+
});
|
|
152
|
+
window.addEventListener("pagehide", function () {
|
|
153
|
+
liveStream.close();
|
|
154
|
+
window.clearTimeout(liveStale);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
`;
|