ompchamber 0.7.0 → 0.8.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/dist/client/index.html +1 -1
- package/dist/client/static/css/index.5c0447cefe.css +2 -0
- package/dist/client/static/js/async/{3y.4c2596f64c.js → 3y.71f2ccf3b6.js} +2 -2
- package/dist/client/static/js/async/{xw.ae67ba06fd.js → 5b.0d6ae50fe8.js} +10 -10
- package/dist/client/static/js/async/5bh.8b2363424f.js +1 -0
- package/dist/client/static/js/async/{vy.bbe114c4e0.js → bi.09688ba75c.js} +1 -2
- package/dist/client/static/js/async/c.797d933882.js +1 -0
- package/dist/client/static/js/async/{dc.6dc4b0a04b.js → dc.856fda6560.js} +2 -2
- package/dist/client/static/js/async/{g.86e2256a70.js → g.d1c2fe09aa.js} +2 -2
- package/dist/client/static/js/async/g.d1c2fe09aa.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{m.4b29d34f93.js → m.0896dab823.js} +2 -2
- package/dist/client/static/js/async/ma.2c261b04dd.js +2 -0
- package/dist/client/static/js/async/nam.b5663e919f.js +10 -0
- package/dist/client/static/js/async/nam.b5663e919f.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{q.542c184fec.js → q.23af2a358f.js} +3 -3
- package/dist/client/static/js/async/{s.413c2a4683.js → s.222ee974f6.js} +2 -2
- package/dist/client/static/js/async/t.4dca0e2ba8.js +1 -0
- package/dist/client/static/js/async/uf.46ad6209fa.js +2 -0
- package/dist/client/static/js/async/x4.69ec69c61b.js +6 -0
- package/dist/client/static/js/async/xn.6360379f71.js +2 -0
- package/dist/client/static/js/async/xnl.feb0021e50.js +2 -0
- package/dist/client/static/js/index.dc5f5fabe8.js +33 -0
- package/dist/client/static/js/{uy.2e8929ed0c.js → jp.50817896f3.js} +6 -6
- package/package.json +1 -1
- package/src/client/components/common/MarkdownRenderer.tsx +69 -27
- package/src/client/components/common/code-editor/history.test.ts +118 -0
- package/src/client/components/common/code-editor/history.ts +101 -0
- package/src/client/components/common/code-editor/index.tsx +56 -66
- package/src/client/components/common/code-surface/index.tsx +84 -11
- package/src/client/components/common/code-surface/measure.ts +66 -17
- package/src/client/components/common/diagram-viewer/index.tsx +258 -0
- package/src/client/components/common/diagram-viewer/read-block.ts +53 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +1 -0
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +10 -3
- package/src/client/components/settings/LazyModal.tsx +1 -1
- package/src/client/components/settings/Modal.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +8 -1
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +11 -52
- package/src/client/components/workspace/chat-timeline/chat-input/selection.ts +57 -0
- package/src/client/components/workspace/chat-timeline/index.tsx +121 -107
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/FrameControl.tsx +172 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +155 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/drafts.ts +113 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +92 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Footer.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Header.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Lazy.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/SelectBody.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/index.tsx +16 -10
- package/src/client/components/workspace/chat-timeline/tool-renderers/index.tsx +2 -2
- package/src/client/components/workspace/file-explorer/index.tsx +18 -5
- package/src/client/hooks/chat/omp/session-list.tsx +4 -0
- package/src/client/hooks/chat/timeline/actions.ts +0 -9
- package/src/client/hooks/chat/timeline/ask-frames.ts +102 -0
- package/src/client/hooks/chat/timeline/extension-dialog.ts +12 -9
- package/src/client/hooks/chat/timeline/index.ts +6 -8
- package/src/client/hooks/chat/timeline/prompt-text.ts +35 -0
- package/src/client/hooks/chat/timeline/send.ts +13 -27
- package/src/client/hooks/chat/timeline/session-load.ts +48 -106
- package/src/client/hooks/chat/timeline/session-pagination.ts +144 -0
- package/src/client/hooks/editor/use-code-window.ts +118 -0
- package/src/client/hooks/ui/input-required-alert.ts +43 -0
- package/src/client/hooks/ui/notification-sound.ts +65 -0
- package/src/client/hooks/ui/theme.ts +22 -6
- package/src/client/tailwind.css +38 -12
- package/src/server/lib/omp/rpc/manager.ts +14 -3
- package/src/server/lib/omp/rpc/session-registry.ts +18 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +7 -1
- package/src/shared/lib/chat/ask-questions.test.ts +169 -0
- package/src/shared/lib/chat/ask-questions.ts +264 -0
- package/src/shared/lib/code/lazy-window.test.ts +161 -0
- package/src/shared/lib/code/lazy-window.ts +156 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +50 -1
- package/src/shared/lib/code/syntax-highlight.ts +14 -1
- package/src/shared/lib/code/windowed-highlight.ts +63 -0
- package/src/shared/lib/markdown/mermaid.ts +8 -6
- package/src/shared/types/omp/agent.ts +2 -2
- package/src/shared/types/workspace.ts +4 -0
- package/dist/client/static/css/index.51f68210a5.css +0 -2
- package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
- package/dist/client/static/js/async/c.cd45b0ac45.js +0 -1
- package/dist/client/static/js/async/fw.6a51998a8e.js +0 -6
- package/dist/client/static/js/async/kc.bbdb35b771.js +0 -1
- package/dist/client/static/js/async/mw.ca1512563c.js +0 -10
- package/dist/client/static/js/async/rf.d626afc367.js +0 -2
- package/dist/client/static/js/async/t.cda4891526.js +0 -1
- package/dist/client/static/js/async/uf.24ca3d49c3.js +0 -2
- package/dist/client/static/js/async/v0.394ed37c6b.js +0 -2
- package/dist/client/static/js/async/v0.394ed37c6b.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/vy.bbe114c4e0.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/xn.afecebbbd9.js +0 -2
- package/dist/client/static/js/index.eccad0b70a.js +0 -33
- package/dist/client/static/js/index.eccad0b70a.js.LICENSE.txt +0 -4
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Ask.tsx +0 -205
- /package/dist/client/static/js/async/{3y.4c2596f64c.js.LICENSE.txt → 3y.71f2ccf3b6.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.86e2256a70.js.LICENSE.txt → 5b.0d6ae50fe8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{dc.6dc4b0a04b.js.LICENSE.txt → dc.856fda6560.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{fw.6a51998a8e.js.LICENSE.txt → m.0896dab823.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{m.4b29d34f93.js.LICENSE.txt → ma.2c261b04dd.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{mw.ca1512563c.js.LICENSE.txt → q.23af2a358f.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.542c184fec.js.LICENSE.txt → s.222ee974f6.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{rf.d626afc367.js.LICENSE.txt → uf.46ad6209fa.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{s.413c2a4683.js.LICENSE.txt → x4.69ec69c61b.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{uf.24ca3d49c3.js.LICENSE.txt → xn.6360379f71.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{xn.afecebbbd9.js.LICENSE.txt → xnl.feb0021e50.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{async/xw.ae67ba06fd.js.LICENSE.txt → index.dc5f5fabe8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{uy.2e8929ed0c.js.LICENSE.txt → jp.50817896f3.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Line geometry and windowing policy for the lazy editor surface.
|
|
8
|
+
*
|
|
9
|
+
* Shiki tokenization is linear in document size (≈0.14 ms per line), so a
|
|
10
|
+
* whole-file highlight costs ~0.8 s at 7 000 lines — and the editor pays it on
|
|
11
|
+
* every keystroke, on top of one gutter node and one wrapped-row measurement
|
|
12
|
+
* per line. A long document therefore renders only the lines the reader can
|
|
13
|
+
* actually see, and this module is the DOM-free half of that: px offsets per
|
|
14
|
+
* line, the viewport → line-range mapping, and the blank space the unrendered
|
|
15
|
+
* lines take up so the caret never leaves its row.
|
|
16
|
+
*
|
|
17
|
+
* The browser measurements this needs (row height, per-line wrapped heights)
|
|
18
|
+
* stay in the surface, which feeds them in as a `LineGeometry`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Documents at or below this size render whole — windowing them saves less than it costs. */
|
|
22
|
+
export const WHOLE_DOCUMENT_MAX_LINES = 300;
|
|
23
|
+
export const WHOLE_DOCUMENT_MAX_CHARS = 20_000;
|
|
24
|
+
|
|
25
|
+
/** Lines kept beyond the viewport so a fast scroll cannot outrun the window. */
|
|
26
|
+
export const WINDOW_OVERSCAN = 24;
|
|
27
|
+
|
|
28
|
+
/** Lines rendered before the first measurement lands; the surface corrects this in a layout effect. */
|
|
29
|
+
export const INITIAL_WINDOW_LINES = 200;
|
|
30
|
+
|
|
31
|
+
/** Half-open line range `[start, end)`. */
|
|
32
|
+
export interface LineWindow {
|
|
33
|
+
start: number;
|
|
34
|
+
end: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Blank space (px) the unrendered lines occupy above and below a window. */
|
|
38
|
+
export interface WindowSpace {
|
|
39
|
+
top: number;
|
|
40
|
+
bottom: number;
|
|
41
|
+
/** The window reaches the document's last line, so it keeps the editor's trailing line break. */
|
|
42
|
+
breakAtEnd: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A windowed render request: the lines to draw plus the space the rest of the document takes. */
|
|
46
|
+
export interface CodeWindow extends LineWindow, WindowSpace {}
|
|
47
|
+
|
|
48
|
+
/** Per-line vertical layout of a document, in px. */
|
|
49
|
+
export interface LineGeometry {
|
|
50
|
+
readonly lineCount: number;
|
|
51
|
+
/** Height (px) of a single visual row. */
|
|
52
|
+
readonly rowHeight: number;
|
|
53
|
+
/** Height (px) of every line together. */
|
|
54
|
+
readonly totalHeight: number;
|
|
55
|
+
/** Y offset (px) of the top edge of line `index`; `lineCount` is the document's end. */
|
|
56
|
+
offsetOf(index: number): number;
|
|
57
|
+
/** Index of the line covering `y`, clamped to the document. */
|
|
58
|
+
lineAt(y: number): number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function clamp(value: number, min: number, max: number): number {
|
|
62
|
+
return value < min ? min : value > max ? max : value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Whether a document is long enough that rendering all of it would be felt. */
|
|
66
|
+
export function shouldWindow(lineCount: number, charCount: number): boolean {
|
|
67
|
+
return lineCount > WHOLE_DOCUMENT_MAX_LINES || charCount > WHOLE_DOCUMENT_MAX_CHARS;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Word wrap off: every line is exactly one row, so no measurement is involved. */
|
|
71
|
+
export function uniformGeometry(rowHeight: number, lineCount: number): LineGeometry {
|
|
72
|
+
return {
|
|
73
|
+
lineCount,
|
|
74
|
+
rowHeight,
|
|
75
|
+
totalHeight: rowHeight * lineCount,
|
|
76
|
+
offsetOf: (index) => rowHeight * clamp(index, 0, lineCount),
|
|
77
|
+
lineAt: (y) => clamp(Math.floor(y / rowHeight), 0, Math.max(lineCount - 1, 0)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Word wrap on: heights are the measured wrapped heights, one entry per line.
|
|
83
|
+
* Returns `null` when they do not describe every line, which leaves the caller
|
|
84
|
+
* on the whole-document path rather than spacing lines with a wrong geometry.
|
|
85
|
+
*/
|
|
86
|
+
export function measuredGeometry(heights: number[], rowHeight: number, lineCount: number): LineGeometry | null {
|
|
87
|
+
if (lineCount === 0 || heights.length !== lineCount) return null;
|
|
88
|
+
const offsets = new Float64Array(lineCount + 1);
|
|
89
|
+
for (let i = 0; i < lineCount; i++) offsets[i + 1] = offsets[i] + heights[i];
|
|
90
|
+
const total = offsets[lineCount];
|
|
91
|
+
return {
|
|
92
|
+
lineCount,
|
|
93
|
+
rowHeight,
|
|
94
|
+
totalHeight: total,
|
|
95
|
+
offsetOf: (index) => offsets[clamp(index, 0, lineCount)],
|
|
96
|
+
lineAt: (y) => {
|
|
97
|
+
if (y < 0) return 0;
|
|
98
|
+
if (total <= 0 || y >= total) return lineCount - 1;
|
|
99
|
+
// Last line whose top edge sits at or above `y`.
|
|
100
|
+
let low = 0;
|
|
101
|
+
let high = lineCount - 1;
|
|
102
|
+
while (low < high) {
|
|
103
|
+
const mid = (low + high + 1) >> 1;
|
|
104
|
+
if (offsets[mid] <= y) low = mid;
|
|
105
|
+
else high = mid - 1;
|
|
106
|
+
}
|
|
107
|
+
return low;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The lines a viewport band (`top`, `height`) covers, plus overscan on both sides. */
|
|
113
|
+
export function resolveWindow(
|
|
114
|
+
geometry: LineGeometry,
|
|
115
|
+
top: number,
|
|
116
|
+
height: number,
|
|
117
|
+
overscan: number = WINDOW_OVERSCAN,
|
|
118
|
+
): LineWindow {
|
|
119
|
+
const first = geometry.lineAt(top);
|
|
120
|
+
const last = geometry.lineAt(top + Math.max(height, 0));
|
|
121
|
+
return {
|
|
122
|
+
start: Math.max(0, first - overscan),
|
|
123
|
+
end: Math.min(geometry.lineCount, Math.max(last + 1, first + 1) + overscan),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** What a freshly opened document renders before its viewport has been read. */
|
|
128
|
+
export function firstWindow(lineCount: number): LineWindow {
|
|
129
|
+
return { start: 0, end: clamp(INITIAL_WINDOW_LINES, 1, Math.max(lineCount, 1)) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Keep a window inside the document it belongs to (the state may predate a file switch). */
|
|
133
|
+
export function clampWindow(window: LineWindow, lineCount: number): LineWindow {
|
|
134
|
+
const start = clamp(window.start, 0, Math.max(lineCount - 1, 0));
|
|
135
|
+
return { start, end: clamp(window.end, start + 1, Math.max(lineCount, 1)) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function sameWindow(a: LineWindow | null, b: LineWindow | null): boolean {
|
|
139
|
+
return a === b || (a !== null && b !== null && a.start === b.start && a.end === b.end);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Space the unrendered lines take around `window`, and whether it ends the document. */
|
|
143
|
+
export function windowSpace(geometry: LineGeometry, window: LineWindow): WindowSpace {
|
|
144
|
+
const top = geometry.offsetOf(window.start);
|
|
145
|
+
return {
|
|
146
|
+
top,
|
|
147
|
+
bottom: Math.max(0, geometry.totalHeight - geometry.offsetOf(window.end)),
|
|
148
|
+
breakAtEnd: window.end >= geometry.lineCount,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Clamp a window to its document and resolve the space it leaves around itself. */
|
|
153
|
+
export function codeWindow(geometry: LineGeometry, window: LineWindow): CodeWindow {
|
|
154
|
+
const clamped = clampWindow(window, geometry.lineCount);
|
|
155
|
+
return { ...clamped, ...windowSpace(geometry, clamped) };
|
|
156
|
+
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
import { beforeAll, describe, expect, test } from 'bun:test';
|
|
7
7
|
|
|
8
8
|
import { bootSyntax, onLanguageReady, requestLanguage } from '@/shared/lib/code/highlighter';
|
|
9
|
-
import { getLanguageFromPath, highlightCode } from '@/shared/lib/code/syntax-highlight';
|
|
9
|
+
import { getLanguageFromPath, highlightCode, highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
10
|
+
import { highlightCodeWindow } from '@/shared/lib/code/windowed-highlight';
|
|
10
11
|
|
|
11
12
|
/** Shiki emits `<span class="shiki">…` with dual-theme CSS variables per token. */
|
|
12
13
|
const SHIKI_RE = /<span class="shiki">/;
|
|
@@ -112,6 +113,54 @@ describe('highlightCode', () => {
|
|
|
112
113
|
});
|
|
113
114
|
});
|
|
114
115
|
|
|
116
|
+
describe('highlightCodeWindow', () => {
|
|
117
|
+
const source = Array.from({ length: 120 }, (_, index) =>
|
|
118
|
+
index === 0 ? 'export const first = 1;' : `export const value${index} = ${index}; // line ${index}`,
|
|
119
|
+
).join('\n');
|
|
120
|
+
|
|
121
|
+
/** The wrapper the app's `.shiki span` colour rule depends on. */
|
|
122
|
+
const unwrap = (html: string) => html.replace(/^<span class="shiki">/, '').replace(/<\/span>$/, '');
|
|
123
|
+
|
|
124
|
+
test('wraps the window in the class the token colours are scoped to', () => {
|
|
125
|
+
const window = highlightCodeWindow(source, 'typescript', { start: 0, end: 3 });
|
|
126
|
+
|
|
127
|
+
// Without this class no `.shiki span` rule matches, so the tokens would
|
|
128
|
+
// render in the plain ink colour.
|
|
129
|
+
expect(window).toMatch(SHIKI_RE);
|
|
130
|
+
expect(window.endsWith('</span>')).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('returns only the requested lines, joined as the editor renders them', () => {
|
|
134
|
+
const window = highlightCodeWindow(source, 'typescript', { start: 10, end: 13 });
|
|
135
|
+
const rows = highlightLines(source, 'typescript');
|
|
136
|
+
|
|
137
|
+
expect(unwrap(window).split('\n')).toEqual(rows.slice(10, 13));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('keeps multi-line grammar state from the lines above the window', () => {
|
|
141
|
+
const commented = ['/*', ' * a block comment', ' * spanning several lines', ' */', 'const after = 1;'].join('\n');
|
|
142
|
+
const rows = highlightLines(commented, 'typescript');
|
|
143
|
+
// Line 2 sits inside the comment, whose opening line is outside the window.
|
|
144
|
+
const window = highlightCodeWindow(commented, 'typescript', { start: 2, end: 4 });
|
|
145
|
+
|
|
146
|
+
expect(unwrap(window).split('\n')).toEqual(rows.slice(2, 4));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('renders an over-long line as plain text instead of tokenizing it', () => {
|
|
150
|
+
const minified = `const bundled = "${'x'.repeat(200)}";\nconst next = 1;`;
|
|
151
|
+
const window = highlightCodeWindow(minified, 'javascript', { start: 0, end: 2 }, { maxLineLength: 50 });
|
|
152
|
+
|
|
153
|
+
expect(window).toContain('const bundled');
|
|
154
|
+
expect(unwrap(window).split('\n')[0]).not.toContain('--shiki-light:');
|
|
155
|
+
expect(unwrap(window).split('\n')[1]).toContain('--shiki-light:');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('returns nothing for a window outside the document', () => {
|
|
159
|
+
expect(highlightCodeWindow('const a = 1;', 'typescript', { start: 5, end: 9 })).toBe('');
|
|
160
|
+
expect(highlightCodeWindow('const a = 1;', 'typescript', { start: 0, end: 0 })).toBe('');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
115
164
|
describe('getLanguageFromPath', () => {
|
|
116
165
|
test.each([
|
|
117
166
|
['a.ts', 'typescript'],
|
|
@@ -268,12 +268,24 @@ export function highlightJson(code: string): string {
|
|
|
268
268
|
return highlightCode(code, 'json');
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
export interface HighlightLinesOptions {
|
|
272
|
+
/**
|
|
273
|
+
* Lines at or above this length are emitted as plain text instead of being
|
|
274
|
+
* tokenized — the guard a minified bundle (one line of hundreds of KB) needs.
|
|
275
|
+
*/
|
|
276
|
+
maxLineLength?: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
271
279
|
/**
|
|
272
280
|
* Highlight code and return one HTML fragment per line, aligned 1:1 with
|
|
273
281
|
* `code.split('\n')`. Tokenizes the whole input at once so multi-line grammar
|
|
274
282
|
* state (block comments, template literals) survives, unlike per-line calls.
|
|
275
283
|
*/
|
|
276
|
-
export function highlightLines(
|
|
284
|
+
export function highlightLines(
|
|
285
|
+
code: string,
|
|
286
|
+
language = 'javascript',
|
|
287
|
+
options: HighlightLinesOptions = {},
|
|
288
|
+
): string[] {
|
|
277
289
|
const rawLines = code.split('\n');
|
|
278
290
|
if (!code) return rawLines;
|
|
279
291
|
const hl = getHighlighterSync();
|
|
@@ -284,6 +296,7 @@ export function highlightLines(code: string, language = 'javascript'): string[]
|
|
|
284
296
|
lang,
|
|
285
297
|
themes: SHIKI_THEMES,
|
|
286
298
|
defaultColor: false,
|
|
299
|
+
tokenizeMaxLineLength: options.maxLineLength ?? 0,
|
|
287
300
|
});
|
|
288
301
|
if (tokens.length !== rawLines.length) return rawLines.map(escapeCode);
|
|
289
302
|
return tokens.map((lineTokens) =>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LineWindow } from '@/shared/lib/code/lazy-window';
|
|
7
|
+
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Lines tokenized *above* the window and dropped from the result. They only
|
|
11
|
+
* seed the grammar state, so a window that starts inside a block comment or a
|
|
12
|
+
* template literal still tokenizes as a comment / template literal. Tokenizing
|
|
13
|
+
* them costs a fraction of the window itself; carrying the whole document as
|
|
14
|
+
* context would defeat the point of windowing at all.
|
|
15
|
+
*/
|
|
16
|
+
const CONTEXT_LINES = 40;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Lines at or above this length render as plain text. A minified bundle is one
|
|
20
|
+
* line of hundreds of KB, which the tokenizer would chew through on every
|
|
21
|
+
* scroll; the cap keeps that bounded, exactly like Shiki's own
|
|
22
|
+
* `tokenizeMaxLineLength` bail-out.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_LINE_CHARS = 12_000;
|
|
25
|
+
|
|
26
|
+
export interface HighlightCodeWindowOptions {
|
|
27
|
+
/** Lines of grammar context above the window. */
|
|
28
|
+
contextLines?: number;
|
|
29
|
+
/** Length at which a line falls back to plain text. */
|
|
30
|
+
maxLineLength?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Shiki markup for lines `[window.start, window.end)` of `code`, joined with
|
|
35
|
+
* newlines so it can be dropped straight into the editor's `<pre>`. The caller
|
|
36
|
+
* reserves the space of the lines it is not given (`lazy-window`'s
|
|
37
|
+
* `windowSpace`), which is what keeps the caret over its own row.
|
|
38
|
+
*
|
|
39
|
+
* The rows are wrapped in `.shiki` — the app colours tokens through the
|
|
40
|
+
* `.shiki span { color: var(--shiki-light) }` rule (`tailwind.css`), so markup
|
|
41
|
+
* that skipped the wrapper would render in the plain ink colour. Every other
|
|
42
|
+
* consumer of `highlightLines` carries the same class.
|
|
43
|
+
*
|
|
44
|
+
* Editing is unaffected: the `<textarea>` above the highlight layer always
|
|
45
|
+
* holds the full document — only the markup is windowed.
|
|
46
|
+
*/
|
|
47
|
+
export function highlightCodeWindow(
|
|
48
|
+
code: string,
|
|
49
|
+
language: string,
|
|
50
|
+
window: LineWindow,
|
|
51
|
+
options: HighlightCodeWindowOptions = {},
|
|
52
|
+
): string {
|
|
53
|
+
const { contextLines = CONTEXT_LINES, maxLineLength = MAX_LINE_CHARS } = options;
|
|
54
|
+
const lines = code.split('\n');
|
|
55
|
+
const start = Math.min(Math.max(window.start, 0), lines.length);
|
|
56
|
+
const end = Math.min(Math.max(window.end, start), lines.length);
|
|
57
|
+
if (start === end) return '';
|
|
58
|
+
|
|
59
|
+
const contextStart = Math.max(0, start - contextLines);
|
|
60
|
+
const rows = highlightLines(lines.slice(contextStart, end).join('\n'), language, { maxLineLength });
|
|
61
|
+
const offset = start - contextStart;
|
|
62
|
+
return `<span class="shiki">${rows.slice(offset, offset + (end - start)).join('\n')}</span>`;
|
|
63
|
+
}
|
|
@@ -94,7 +94,7 @@ function applySvg(block: HTMLElement, svg: string, themeKey: ThemeKey): void {
|
|
|
94
94
|
const source = block.dataset.mermaid ?? '';
|
|
95
95
|
const copyAttr = source.replace(/"/g, '"');
|
|
96
96
|
block.innerHTML =
|
|
97
|
-
`<button type="button" class="code-copy-float" data-copy="${copyAttr}" aria-label="Copy diagram source">` +
|
|
97
|
+
`<button type="button" class="code-copy-float" data-copy="${copyAttr}" aria-label="Copy diagram source" title="Copy diagram source">` +
|
|
98
98
|
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>' +
|
|
99
99
|
'</button>' +
|
|
100
100
|
svg;
|
|
@@ -142,11 +142,13 @@ async function renderBlock(block: HTMLElement, cachedOnly: boolean): Promise<boo
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
|
-
* Render every pending `.mermaid-block` under `root`.
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
145
|
+
* Render every pending `.mermaid-block` under `root`. A block whose SVG was
|
|
146
|
+
* rendered for a different theme key is re-rendered here, which is how a theme
|
|
147
|
+
* switch repaints diagrams; `force` re-renders even matching blocks.
|
|
148
|
+
* Idempotent — blocks already rendered with the current theme are skipped
|
|
149
|
+
* unless forced, and blocks mid-render are left alone. With `cachedOnly`, only
|
|
150
|
+
* blocks whose SVG is already cached are restored (synchronous, no rendering) —
|
|
151
|
+
* the fast path that keeps remounted blocks from flashing the placeholder.
|
|
150
152
|
*/
|
|
151
153
|
export async function hydrateMermaidBlocks(
|
|
152
154
|
root: ParentNode,
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* omp agent event surface types shared by useOmpAgent, the chamber timeline
|
|
8
|
-
* and the
|
|
9
|
-
* repo's per-file size ceiling.
|
|
8
|
+
* and the extension-dialog renderer. Kept separate so the hook file stays under
|
|
9
|
+
* the repo's per-file size ceiling.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ChatMessageData } from '@/shared/types/chat';
|
|
@@ -17,6 +17,10 @@ export interface SessionItemData {
|
|
|
17
17
|
/** Server-tracked stream state: `stream` while a run is in flight, then a
|
|
18
18
|
* one-shot `finish`/`abort`/`error` badge cleared when the session opens. */
|
|
19
19
|
streamStatus?: 'stream' | 'finish' | 'abort' | 'error';
|
|
20
|
+
/** Live-only: the session's omp process is BLOCKED on an ask/approval dialog
|
|
21
|
+
* waiting for a user answer. Derived from the running process registry, never
|
|
22
|
+
* persisted — the process it describes is the same thing that owns the flag. */
|
|
23
|
+
awaitingInput?: boolean;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/**
|