ompchamber 3.5.0 → 3.6.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/index.js +357 -353
- package/dist/client/static/js/index-7n838fz5.css +1 -0
- package/dist/client/static/js/{index-w0cem7n5.js → index-qp2sfmn2.js} +591 -592
- package/package.json +1 -1
- package/src/cli/lib/commands/status.js +7 -0
- package/src/client/components/layout/session-sidebar/about-modal/UpdateLog.tsx +50 -0
- package/src/client/components/layout/session-sidebar/about-modal/UpdateSection.tsx +12 -17
- package/src/client/components/settings/ProjectSelectorDropdown.tsx +8 -2
- package/src/client/components/settings/SettingsSidebarList.tsx +5 -1
- package/src/client/components/settings/categories/CommandSettings.tsx +47 -9
- package/src/client/components/settings/categories/SkillCatalogSettings.tsx +23 -43
- package/src/client/components/settings/categories/SkillSettings.tsx +51 -7
- package/src/client/components/settings/categories/command-settings/DetailPane.tsx +109 -103
- package/src/client/components/settings/categories/command-settings/SidebarList.tsx +6 -1
- package/src/client/components/settings/categories/skill-settings/DetailPane.tsx +130 -31
- package/src/client/components/settings/categories/skill-settings/SidebarList.tsx +9 -3
- package/src/client/data/settings/command.ts +0 -40
- package/src/client/hooks/settings/workspace-roots.ts +99 -0
- package/src/client/hooks/ui/updates/index.ts +178 -0
- package/src/server/db.server.ts +41 -11
- package/src/server/index.ts +17 -1
- package/src/server/lib/db/client.ts +17 -0
- package/src/server/lib/db/compact-chat-overlay.test.ts +121 -0
- package/src/server/lib/db/compact-chat-overlay.ts +90 -0
- package/src/server/lib/lifecycle/fd-pressure.test.ts +66 -0
- package/src/server/lib/lifecycle/fd-pressure.ts +159 -0
- package/src/server/lib/omp/config/commands.test.ts +167 -0
- package/src/server/lib/omp/config/commands.ts +195 -0
- package/src/server/lib/omp/config/scope.test.ts +49 -0
- package/src/server/lib/omp/config/scope.ts +65 -0
- package/src/server/lib/omp/config/skills-catalog.ts +35 -3
- package/src/server/lib/omp/config/skills.test.ts +105 -0
- package/src/server/lib/omp/config/skills.ts +260 -47
- package/src/server/lib/omp/rpc/process.ts +22 -14
- package/src/server/lib/omp/rpc/session-registry.ts +6 -0
- package/src/server/lib/omp/rpc/utility.ts +45 -23
- package/src/server/lib/omp/session/merge-stored-anchor.test.ts +85 -0
- package/src/server/lib/omp/session/merge-stored-overlay-rows.test.ts +106 -0
- package/src/server/lib/omp/session/merge-stored.ts +78 -11
- package/src/server/lib/omp/session/reload.server.test.ts +84 -0
- package/src/server/lib/omp/session/reload.server.ts +47 -0
- package/src/server/lib/omp/session/subagent-presence.ts +40 -9
- package/src/server/lib/terminal/liveness.server.ts +65 -0
- package/src/server/lib/terminal/runtime.server.ts +70 -117
- package/src/server/lib/terminal/session.server.ts +5 -2
- package/src/server/lib/terminal/store.ts +72 -0
- package/src/server/lib/updates/apply.ts +62 -33
- package/src/server/lib/updates/install.ts +9 -2
- package/src/server/lib/updates/omp.ts +37 -9
- package/src/server/lib/updates/single-flight.test.ts +61 -0
- package/src/server/lib/updates/single-flight.ts +68 -0
- package/src/server/routes/chat/session.ts +13 -3
- package/src/server/routes/health/probe.ts +8 -0
- package/src/server/routes/omp/index.ts +0 -2
- package/src/server/routes/settings/commands.ts +102 -15
- package/src/server/routes/settings/skills.ts +165 -113
- package/src/server/routes/terminal/sessions.ts +1 -1
- package/src/server/routes/updates/apply.ts +101 -14
- package/src/shared/lib/chat/composer/client.ts +27 -18
- package/src/shared/lib/chat/composer/scope.test.ts +87 -0
- package/src/shared/lib/markdown/katex.ts +11 -1
- package/src/shared/lib/markdown/marked.ts +68 -13
- package/src/shared/lib/markdown/math.test.ts +53 -1
- package/src/shared/lib/updates/progress.test.ts +54 -0
- package/src/shared/lib/updates/progress.ts +50 -0
- package/src/shared/types/settings/command.ts +15 -2
- package/src/shared/types/settings/skill.ts +13 -0
- package/dist/client/static/js/index-vx0ernrv.css +0 -1
- package/src/client/hooks/ui/updates.ts +0 -108
- package/src/server/routes/omp/commands.ts +0 -29
|
@@ -62,7 +62,17 @@ export function preloadKatex(): void {
|
|
|
62
62
|
* like every other model-authored fragment in this pipeline.
|
|
63
63
|
*/
|
|
64
64
|
function renderPlaceholder(el: HTMLElement, katex: KatexModule['default']): void {
|
|
65
|
-
|
|
65
|
+
// `marked.ts` percent-encodes the payload, the way a mermaid block carries
|
|
66
|
+
// its source: the TeX must survive the HTML attribute round-trip untouched,
|
|
67
|
+
// and escaping it as HTML double-encoded every formula the prose escaper had
|
|
68
|
+
// already touched. A malformed payload degrades to the raw attribute.
|
|
69
|
+
const encoded = el.dataset.math ?? '';
|
|
70
|
+
let tex = encoded;
|
|
71
|
+
try {
|
|
72
|
+
tex = decodeURIComponent(encoded);
|
|
73
|
+
} catch {
|
|
74
|
+
// Leave `tex` as the literal attribute text.
|
|
75
|
+
}
|
|
66
76
|
const displayMode = el.dataset.mathDisplay === '1';
|
|
67
77
|
try {
|
|
68
78
|
el.innerHTML = sanitizeKatexHtml(katex.renderToString(tex, { throwOnError: false, displayMode }));
|
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
* 2. marked — GFM true, breaks true, autolink via linkify-it (CJK
|
|
12
12
|
* punctuation is not swallowed into URLs, and a match carrying a backtick
|
|
13
13
|
* is left to the code-span rule — see autolink.ts), plus a KaTeX
|
|
14
|
-
* extension for \(...\) inline, \[...\] block,
|
|
14
|
+
* extension for \(...\) inline, \[...\] block, $$...$$ display and
|
|
15
|
+
* $...$ inline math
|
|
16
|
+
*
|
|
17
|
+
* remend heals BLOCK math (`$$…$$`) only. Its inline healer must stay off
|
|
18
|
+
* (`inlineKatex: false`): it closes an unmatched `$` by appending one to the
|
|
19
|
+
* END OF THE TEXT, so any message with an odd `$` count — every lone `$HOME`,
|
|
20
|
+
* `$PATH`, `$1` — became one giant formula from that `$` to the end of the
|
|
21
|
+
* message, and a `$` landing on a closing ``` fence swallowed the rest of the
|
|
22
|
+
* document into that code block. `$…$` is left to marked's own flanking rules.
|
|
15
23
|
*
|
|
16
24
|
* The resulting HTML is NOT sanitized here — run the output through
|
|
17
25
|
* DOMPurify (see sanitize.ts) before injecting. What the source's own HTML
|
|
@@ -35,19 +43,44 @@ function escapeHtml(value: string): string {
|
|
|
35
43
|
.replace(/'/g, ''');
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
/**
|
|
47
|
+
* True while `renderMarkdown` parses prose it escaped itself. Set around the
|
|
48
|
+
* `marked.parse` call, read by `renderMathPlaceholder`. Synchronous by
|
|
49
|
+
* construction: `marked.parse` never awaits, so no nested `renderMarkdown`
|
|
50
|
+
* can observe the flag mid-parse.
|
|
51
|
+
*/
|
|
52
|
+
let proseEscaped = false;
|
|
53
|
+
|
|
38
54
|
/**
|
|
39
55
|
* Emit a math placeholder instead of rendering KaTeX inline.
|
|
40
56
|
*
|
|
41
57
|
* `katex` is ~522 kB of JS worth loading only when a message contains math, so
|
|
42
58
|
* it is not imported here. The placeholder carries the TeX source and the
|
|
43
59
|
* display flag; `katex.ts` swaps it for real markup once the library lands
|
|
44
|
-
* (see `hydrateMathBlocks`).
|
|
45
|
-
*
|
|
60
|
+
* (see `hydrateMathBlocks`).
|
|
61
|
+
*
|
|
62
|
+
* Two things happen to the captured source here, and both exist because this
|
|
63
|
+
* renderer is the one place all four math extensions funnel through:
|
|
64
|
+
*
|
|
65
|
+
* 1. **Entities are reversed** when `renderMarkdown` escaped the prose itself
|
|
66
|
+
* (`proseEscaped`). `escapeHtmlOutsideCode` rewrites the source BEFORE
|
|
67
|
+
* marked sees it, so a tokenizer captures `x < y` rather than `x < y` and
|
|
68
|
+
* KaTeX reports a syntax error for a perfectly good formula. Only the three
|
|
69
|
+
* entities that escaper emits are reversed, and only when it ran — a
|
|
70
|
+
* document (`allowHtml`) whose own text legitimately holds `&` is left
|
|
71
|
+
* alone.
|
|
72
|
+
* 2. **The result is percent-encoded**, the way a mermaid block carries its
|
|
73
|
+
* source. HTML-escaping the payload instead re-escapes what step 1 just
|
|
74
|
+
* restored, and the attribute round-trip hides the extra layer from both
|
|
75
|
+
* ends. An encoded payload holds no HTML-special character, so nothing
|
|
76
|
+
* between here and `renderPlaceholder` can alter it.
|
|
46
77
|
*/
|
|
47
78
|
function renderMathPlaceholder(tex: string, displayMode: boolean): string {
|
|
48
|
-
const
|
|
79
|
+
const restored = proseEscaped
|
|
80
|
+
? tex.replace(/&(amp|lt|gt);/g, (_, name: string) => (name === 'amp' ? '&' : name === 'lt' ? '<' : '>'))
|
|
81
|
+
: tex;
|
|
49
82
|
const display = displayMode ? ' data-math-display="1"' : '';
|
|
50
|
-
const span = `<span class="${MATH_PENDING_CLASS}" data-math="${
|
|
83
|
+
const span = `<span class="${MATH_PENDING_CLASS}" data-math="${encodeURIComponent(restored)}"${display}></span>`;
|
|
51
84
|
// Block math must stay inside a block element: DOMPurify drops a bare
|
|
52
85
|
// `<span>` that sits at the top level of the fragment, which silently deleted
|
|
53
86
|
// every `$$…$$` formula. KaTeX's own output is inline-level too, so the old
|
|
@@ -178,11 +211,25 @@ marked.use({
|
|
|
178
211
|
return renderMathPlaceholder(token.text ?? '', true);
|
|
179
212
|
},
|
|
180
213
|
},
|
|
181
|
-
// $ ... $ inline math
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
214
|
+
// $ ... $ inline math. The flanking rules are the whole defence: a single
|
|
215
|
+
// `$` is the one delimiter that collides with ordinary text, and this
|
|
216
|
+
// pipeline renders shell output, currency, SQL and awk programs all day.
|
|
217
|
+
//
|
|
218
|
+
// opening $ — not preceded by a word char (checked in `start`, the only
|
|
219
|
+
// place the preceding character is visible), and not
|
|
220
|
+
// followed by a digit, `$` or `{`. The digit guard is what
|
|
221
|
+
// makes `$5`, `$1` and `$10` text; `{` keeps `${HOME}` out.
|
|
222
|
+
// closing $ — not preceded by whitespace, and not followed by a WORD
|
|
223
|
+
// char or `$`. That trailing guard is load-bearing twice
|
|
224
|
+
// over: it rejects `PATH=$PATH:$HOME` (closer followed by
|
|
225
|
+
// `H`) and `a$b$c` (followed by `c`), and it is the reason
|
|
226
|
+
// the rule no longer depends on where `start` happened to
|
|
227
|
+
// land — the tokenizer is anchored at `^` and cannot see
|
|
228
|
+
// the preceding character, so `a$b$c` was math while
|
|
229
|
+
// `ax$b$c` was text on the identical rule.
|
|
230
|
+
//
|
|
231
|
+
// `$n$ = 3` and `$x^2$ benar` still parse: a space after the closer is not
|
|
232
|
+
// a word char.
|
|
186
233
|
{
|
|
187
234
|
name: 'inlineMathDollar',
|
|
188
235
|
level: 'inline',
|
|
@@ -191,7 +238,7 @@ marked.use({
|
|
|
191
238
|
return match ? match.index + (match[1]?.length ?? 0) : undefined;
|
|
192
239
|
},
|
|
193
240
|
tokenizer(src: string) {
|
|
194
|
-
const match = /^\$(
|
|
241
|
+
const match = /^\$(?![\d${])((?:\\\$|[^$])*?)(?<!\s)\$(?![\w$])/.exec(src);
|
|
195
242
|
return match
|
|
196
243
|
? { type: 'inlineMathDollar', raw: match[0], text: match[1] }
|
|
197
244
|
: undefined;
|
|
@@ -241,9 +288,17 @@ export interface RenderMarkdownOptions {
|
|
|
241
288
|
*/
|
|
242
289
|
export function renderMarkdown(markdown: string, options: RenderMarkdownOptions = {}): string {
|
|
243
290
|
const source = options.allowHtml ? markdown : escapeHtmlOutsideCode(markdown);
|
|
244
|
-
const healed = remend(source, { katex: true, inlineKatex:
|
|
291
|
+
const healed = remend(source, { katex: true, inlineKatex: false })
|
|
245
292
|
.replace(INCOMPLETE_LINK_RE, (_, text: string) => `<span class="md-incomplete-link">${text}</span>`);
|
|
246
|
-
|
|
293
|
+
// The math renderers need to know the prose was escaped, so they can hand
|
|
294
|
+
// KaTeX the source the author wrote instead of `x < y`. `marked.parse`
|
|
295
|
+
// never awaits, so no nested render can observe this mid-parse.
|
|
296
|
+
proseEscaped = !options.allowHtml;
|
|
297
|
+
try {
|
|
298
|
+
return (marked.parse(healed) as string).trim();
|
|
299
|
+
} finally {
|
|
300
|
+
proseEscaped = false;
|
|
301
|
+
}
|
|
247
302
|
}
|
|
248
303
|
|
|
249
304
|
export { marked };
|
|
@@ -78,7 +78,9 @@ describe('lazy math rendering', () => {
|
|
|
78
78
|
|
|
79
79
|
const placeholder = host.querySelector('.math-pending');
|
|
80
80
|
expect(placeholder).not.toBeNull();
|
|
81
|
-
|
|
81
|
+
// The payload travels percent-encoded (see `renderMathPlaceholder`), so the
|
|
82
|
+
// assertion decodes rather than reading the attribute raw.
|
|
83
|
+
expect(decodeURIComponent(placeholder?.getAttribute('data-math') ?? '')).toBe('E = mc^2');
|
|
82
84
|
|
|
83
85
|
expect(await hydrateMathBlocks(host)).toEqual({ rendered: 1, failed: 0 });
|
|
84
86
|
|
|
@@ -123,4 +125,54 @@ describe('lazy math rendering', () => {
|
|
|
123
125
|
const html = renderMarkdown('a $x" onmouseover="alert(1)$ b');
|
|
124
126
|
expect(html).not.toContain('onmouseover="alert');
|
|
125
127
|
});
|
|
128
|
+
|
|
129
|
+
// A lone `$` is the delimiter most likely to appear in ordinary text this
|
|
130
|
+
// app renders all day — shell output, currency, SQL placeholders, awk
|
|
131
|
+
// programs. Each of these was a formula before the flanking rules and the
|
|
132
|
+
// remend inline healer were fixed, and the failure was not cosmetic: an odd
|
|
133
|
+
// `$` count made the formula run to the END of the message.
|
|
134
|
+
test('a single $ is text, never math', () => {
|
|
135
|
+
for (const source of [
|
|
136
|
+
'Hasil: $HOME',
|
|
137
|
+
'$HOME',
|
|
138
|
+
'Path ada di $HOME.',
|
|
139
|
+
'Biaya $5.',
|
|
140
|
+
'Pakai ${HOME}/bin',
|
|
141
|
+
'SELECT * FROM t WHERE id = $1',
|
|
142
|
+
"awk '{print $1, $2}' file",
|
|
143
|
+
'PATH=$PATH:$HOME',
|
|
144
|
+
'a$b$c',
|
|
145
|
+
]) {
|
|
146
|
+
const host = mount(source);
|
|
147
|
+
expect(host.querySelectorAll('.math-pending').length).toBe(0);
|
|
148
|
+
expect(host.textContent).toBe(source);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('an unmatched $ does not run to the end of the message', () => {
|
|
153
|
+
// remend's inline healer used to close the `$` at the very end of the
|
|
154
|
+
// text, turning everything after it into one formula.
|
|
155
|
+
const source = 'Hasil: $HOME lalu baris kedua biasa';
|
|
156
|
+
const host = mount(source);
|
|
157
|
+
expect(host.querySelectorAll('.math-pending').length).toBe(0);
|
|
158
|
+
expect(host.textContent).toBe(source);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('a $ in a fenced block leaves the closing fence intact', () => {
|
|
162
|
+
const host = mount('```\necho $HOME\n```');
|
|
163
|
+
const code = host.querySelector('pre code');
|
|
164
|
+
expect(code?.textContent).toBe('echo $HOME');
|
|
165
|
+
expect(host.querySelectorAll('.math-pending').length).toBe(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// `escapeHtmlOutsideCode` rewrites the source before marked parses it, so a
|
|
169
|
+
// formula reached KaTeX as `x < y` and rendered as a syntax error.
|
|
170
|
+
test('entities in a formula reach KaTeX as the author wrote them', async () => {
|
|
171
|
+
const host = mount('rumus $x < y$ dan $a \\& b$ benar');
|
|
172
|
+
expect(host.querySelectorAll('.math-pending').length).toBe(2);
|
|
173
|
+
|
|
174
|
+
await hydrateMathBlocks(host);
|
|
175
|
+
expect(host.querySelectorAll('.katex-error').length).toBe(0);
|
|
176
|
+
expect(host.querySelectorAll('.katex').length).toBe(2);
|
|
177
|
+
});
|
|
126
178
|
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The update log fold. Output arrives in chunks that do not respect line
|
|
3
|
+
* boundaries, so the two properties that matter are: a line is never rendered
|
|
4
|
+
* in halves, and a long log keeps its tail.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
|
|
9
|
+
import { appendUpdateLog, UPDATE_LOG_LIMIT, updateLogText, type UpdateLogState } from '@/shared/lib/updates/progress';
|
|
10
|
+
|
|
11
|
+
function empty(): UpdateLogState {
|
|
12
|
+
return { lines: [], pending: '', truncated: false };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('appendUpdateLog', () => {
|
|
16
|
+
test('a chunk split mid-line is held until its newline arrives', () => {
|
|
17
|
+
const first = appendUpdateLog(empty(), 'Installing ompcha');
|
|
18
|
+
// Nothing complete yet: the partial line is carried, not rendered as a line.
|
|
19
|
+
expect(first.lines).toEqual([]);
|
|
20
|
+
expect(updateLogText(first)).toBe('Installing ompcha');
|
|
21
|
+
|
|
22
|
+
const second = appendUpdateLog(first, 'mber@0.7.1\n');
|
|
23
|
+
expect(second.lines).toEqual(['Installing ompchamber@0.7.1']);
|
|
24
|
+
expect(second.pending).toBe('');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('several lines in one chunk each become their own line', () => {
|
|
28
|
+
const state = appendUpdateLog(empty(), 'one\ntwo\nthree');
|
|
29
|
+
expect(state.lines).toEqual(['one', 'two']);
|
|
30
|
+
expect(state.pending).toBe('three');
|
|
31
|
+
expect(updateLogText(state)).toBe('one\ntwo\nthree');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('an empty chunk does not add a line', () => {
|
|
35
|
+
const state = appendUpdateLog(appendUpdateLog(empty(), 'x\n'), '');
|
|
36
|
+
expect(state.lines).toEqual(['x']);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('a log past the limit keeps the newest lines and reports the trim', () => {
|
|
40
|
+
const chunk = `${'a'.repeat(1000)}\n`.repeat(30);
|
|
41
|
+
const state = appendUpdateLog(empty(), chunk);
|
|
42
|
+
|
|
43
|
+
expect(state.truncated).toBe(true);
|
|
44
|
+
const length = state.lines.reduce((total, line) => total + line.length + 1, 0);
|
|
45
|
+
expect(length).toBeLessThanOrEqual(UPDATE_LOG_LIMIT);
|
|
46
|
+
// The tail is what survives: a failing step reports at the end.
|
|
47
|
+
expect(state.lines.length).toBeLessThan(30);
|
|
48
|
+
expect(state.lines.at(-1)).toBe('a'.repeat(1000));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('a log under the limit is not marked truncated', () => {
|
|
52
|
+
expect(appendUpdateLog(empty(), 'short\n').truncated).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The log fold behind the update stream.
|
|
3
|
+
*
|
|
4
|
+
* Command output arrives in arbitrary chunks, so the state has to hold a
|
|
5
|
+
* partial trailing line across frames — rendering a half-line as if it were
|
|
6
|
+
* finished makes the log flicker between two spellings of the same line. The
|
|
7
|
+
* fold lives here, apart from the hook, so it is unit-testable without a
|
|
8
|
+
* process or a network.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Cap on retained output; the tail is what matters when a step fails. */
|
|
12
|
+
export const UPDATE_LOG_LIMIT = 20_000;
|
|
13
|
+
|
|
14
|
+
export interface UpdateLogState {
|
|
15
|
+
/** Lines complete enough to show. */
|
|
16
|
+
lines: string[];
|
|
17
|
+
/** Carried across frames: a chunk boundary can fall mid-line. */
|
|
18
|
+
pending: string;
|
|
19
|
+
/** Bytes were dropped from the head once the log passed the limit. */
|
|
20
|
+
truncated: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Fold one streamed chunk into the log. */
|
|
24
|
+
export function appendUpdateLog(state: UpdateLogState, chunk: string): UpdateLogState {
|
|
25
|
+
const parts = (state.pending + chunk).split('\n');
|
|
26
|
+
const pending = parts.pop() ?? '';
|
|
27
|
+
if (parts.length === 0) return { ...state, pending };
|
|
28
|
+
|
|
29
|
+
let lines = [...state.lines, ...parts];
|
|
30
|
+
let truncated = state.truncated;
|
|
31
|
+
let length = lines.reduce((total, line) => total + line.length + 1, 0);
|
|
32
|
+
let start = 0;
|
|
33
|
+
// Keep the newest lines: a failing step reports at the end, and the head of a
|
|
34
|
+
// long install log is the least useful thing on screen.
|
|
35
|
+
while (length > UPDATE_LOG_LIMIT && start < lines.length - 1) {
|
|
36
|
+
length -= (lines[start]?.length ?? 0) + 1;
|
|
37
|
+
start += 1;
|
|
38
|
+
}
|
|
39
|
+
if (start > 0) {
|
|
40
|
+
lines = lines.slice(start);
|
|
41
|
+
truncated = true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { lines, pending, truncated };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The text to show: complete lines, then whatever partial line is in flight. */
|
|
48
|
+
export function updateLogText(state: UpdateLogState): string {
|
|
49
|
+
return (state.pending ? [...state.lines, state.pending] : state.lines).join('\n');
|
|
50
|
+
}
|
|
@@ -12,10 +12,23 @@ export interface CommandItem {
|
|
|
12
12
|
name: string;
|
|
13
13
|
description: string;
|
|
14
14
|
scope: CommandScope;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Markdown template. For a command backed by a file this is the file body
|
|
17
|
+
* (`$ARGUMENTS`, `` !`shell` `` and `@file` are expanded by omp at run time);
|
|
18
|
+
* for a builtin it is the literal `/name` token.
|
|
19
|
+
*/
|
|
17
20
|
template: string;
|
|
18
21
|
isBuiltIn?: boolean;
|
|
22
|
+
/** Absolute path of the markdown command file backing this command, when it
|
|
23
|
+
* is one. Absent for a builtin, a skill, or an extension command. */
|
|
24
|
+
filePath?: string;
|
|
25
|
+
/** omp's discovery source: `builtin`, `skill`, `custom`, `extension`, `file`. */
|
|
26
|
+
source?: string;
|
|
27
|
+
/** True only for a markdown file under a root the chamber owns
|
|
28
|
+
* (`~/.omp/agent/commands`, `<workspace>/.omp/commands`). Everything else —
|
|
29
|
+
* a builtin, a `.claude` command, an extension command — renders read-only
|
|
30
|
+
* and cannot be deleted through the chamber. */
|
|
31
|
+
managed?: boolean;
|
|
19
32
|
/** oh-my-pi aliases (`/models` for `/model`); live agent commands only. */
|
|
20
33
|
aliases?: string[];
|
|
21
34
|
/** Declarative subcommands; live agent commands only. */
|
|
@@ -7,8 +7,21 @@ export interface SkillItem {
|
|
|
7
7
|
location: SkillLocation;
|
|
8
8
|
locationLabel?: string;
|
|
9
9
|
group?: string;
|
|
10
|
+
/**
|
|
11
|
+
* The SKILL.md body for a skill the chamber owns (`managed`), or the file
|
|
12
|
+
* path for a read-only one — where the settings pane shows the source
|
|
13
|
+
* location instead of an editor it must not save through.
|
|
14
|
+
*/
|
|
10
15
|
instructions: string;
|
|
11
16
|
project: string;
|
|
17
|
+
/** Absolute path of the SKILL.md backing this skill. */
|
|
18
|
+
filePath?: string;
|
|
19
|
+
/** omp's discovery source, e.g. `native:user`, `agents:project`. */
|
|
20
|
+
source?: string;
|
|
21
|
+
/** Chamber-owned: editable and deletable through the settings pane. */
|
|
22
|
+
managed?: boolean;
|
|
23
|
+
/** Excluded from the model's `<skills>` listing (`hide` / disableModelInvocation). */
|
|
24
|
+
hidden?: boolean;
|
|
12
25
|
isInstalledFromCatalog?: boolean;
|
|
13
26
|
catalogSource?: string;
|
|
14
27
|
}
|