ompchamber 0.6.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/README.md +24 -8
- 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.8515cbc6f7.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 +3 -1
- package/src/cli/lib/args.js +1 -1
- package/src/cli/lib/commands/restart.js +11 -0
- package/src/cli/lib/commands/update.js +21 -7
- package/src/cli/lib/runtime.js +6 -3
- 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/index.ts +4 -109
- package/src/client/hooks/chat/omp/prompt-send.ts +161 -0
- 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 +16 -29
- 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/index.ts +4 -3
- package/src/server/lib/lifecycle/bun.ts +18 -0
- package/src/server/lib/lifecycle/launch-mode.test.ts +32 -0
- package/src/server/lib/lifecycle/launch-mode.ts +40 -0
- package/src/server/lib/lifecycle/restart.test.ts +21 -0
- package/src/server/lib/lifecycle/restart.ts +111 -0
- package/src/server/lib/omp/rpc/manager.ts +14 -3
- package/src/server/lib/omp/rpc/session-commands.ts +15 -1
- 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/server/lib/updates/apply.ts +8 -5
- package/src/server/lib/updates/install.ts +7 -4
- package/src/server/routes/health/probe.ts +2 -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/lib/omp/session/stream-state.server.ts +16 -1
- 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.47d3b04d5f.js +0 -33
- package/dist/client/static/js/index.47d3b04d5f.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.8515cbc6f7.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
package/src/client/tailwind.css
CHANGED
|
@@ -318,6 +318,11 @@
|
|
|
318
318
|
border-radius: 0.5rem;
|
|
319
319
|
background: var(--theme-paper);
|
|
320
320
|
padding: 0.75rem;
|
|
321
|
+
/* `applySvg` swaps the `.code-block` shell (the positioned ancestor the copy
|
|
322
|
+
button relied on) for the SVG, so the block owns the anchor itself. */
|
|
323
|
+
position: relative;
|
|
324
|
+
/* The block is the door into the zoom/pan viewer. */
|
|
325
|
+
cursor: zoom-in;
|
|
321
326
|
}
|
|
322
327
|
.prose-content .mermaid-block > svg {
|
|
323
328
|
display: block;
|
|
@@ -332,6 +337,27 @@
|
|
|
332
337
|
color: var(--theme-error);
|
|
333
338
|
}
|
|
334
339
|
|
|
340
|
+
/* Full-screen mermaid viewer (`common/diagram-viewer/`): a chart opened from
|
|
341
|
+
the timeline. The stage is the fixed-size pan surface, the canvas child
|
|
342
|
+
carries the zoom/pan transform, so the SVG keeps its natural pixel size and
|
|
343
|
+
`100%` on it always means "the size mermaid rendered". */
|
|
344
|
+
.diagram-viewer-stage {
|
|
345
|
+
touch-action: none;
|
|
346
|
+
overscroll-behavior: contain;
|
|
347
|
+
}
|
|
348
|
+
.diagram-viewer-canvas {
|
|
349
|
+
transform-origin: center center;
|
|
350
|
+
will-change: transform;
|
|
351
|
+
background: var(--theme-paper);
|
|
352
|
+
box-shadow: 0 25px 50px -12px color-mix(in srgb, var(--theme-ink) 45%, transparent);
|
|
353
|
+
}
|
|
354
|
+
.diagram-viewer-canvas > svg {
|
|
355
|
+
display: block;
|
|
356
|
+
width: 100% !important;
|
|
357
|
+
height: 100% !important;
|
|
358
|
+
max-width: none !important;
|
|
359
|
+
}
|
|
360
|
+
|
|
335
361
|
/* PWA Window Controls Overlay and Native Titlebar Styling */
|
|
336
362
|
.titlebar-drag-region {
|
|
337
363
|
-webkit-app-region: drag;
|
|
@@ -344,31 +370,31 @@
|
|
|
344
370
|
}
|
|
345
371
|
|
|
346
372
|
|
|
347
|
-
/*
|
|
348
|
-
Exit is intentionally instant: a 160ms fade-out on a blocking
|
|
349
|
-
load-bearing, and keeping the element mounted to animate it out
|
|
350
|
-
state the dialog does not otherwise need. */
|
|
351
|
-
@keyframes
|
|
373
|
+
/* Blocking extension-dialog enter animation — replaces the retired `motion`
|
|
374
|
+
dependency. Exit is intentionally instant: a 160ms fade-out on a blocking
|
|
375
|
+
modal is not load-bearing, and keeping the element mounted to animate it out
|
|
376
|
+
would add state the dialog does not otherwise need. */
|
|
377
|
+
@keyframes extension-dialog-backdrop-in {
|
|
352
378
|
from { opacity: 0; }
|
|
353
379
|
to { opacity: 1; }
|
|
354
380
|
}
|
|
355
381
|
|
|
356
|
-
@keyframes
|
|
382
|
+
@keyframes extension-dialog-panel-in {
|
|
357
383
|
from { opacity: 0; transform: translateY(10px) scale(0.95); }
|
|
358
384
|
to { opacity: 1; transform: none; }
|
|
359
385
|
}
|
|
360
386
|
|
|
361
|
-
.
|
|
362
|
-
animation:
|
|
387
|
+
.extension-dialog-backdrop {
|
|
388
|
+
animation: extension-dialog-backdrop-in 150ms ease-out;
|
|
363
389
|
}
|
|
364
390
|
|
|
365
|
-
.
|
|
366
|
-
animation:
|
|
391
|
+
.extension-dialog-panel {
|
|
392
|
+
animation: extension-dialog-panel-in 180ms ease-out;
|
|
367
393
|
}
|
|
368
394
|
|
|
369
395
|
@media (prefers-reduced-motion: reduce) {
|
|
370
|
-
.
|
|
371
|
-
.
|
|
396
|
+
.extension-dialog-backdrop,
|
|
397
|
+
.extension-dialog-panel {
|
|
372
398
|
animation: none;
|
|
373
399
|
}
|
|
374
400
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { getDatabasePath } from '@/server/db.server';
|
|
|
6
6
|
import { fetchOmpRegistrySnapshot } from '@/server/lib/models/provider-registry.server';
|
|
7
7
|
import { ompStartupError, ompStartupLogLines } from '@/server/lib/omp/core/startup';
|
|
8
8
|
import { readInstanceRecord, removeInstanceRecord, writeInstanceRecord } from '@/server/lib/lifecycle/instance';
|
|
9
|
+
import { resolveLaunchMode } from '@/server/lib/lifecycle/launch-mode';
|
|
9
10
|
import { acquirePortLock, claimPort, PortInUseError } from '@/server/lib/lifecycle/port-guard';
|
|
10
11
|
import { isMockMode } from '@/server/mock.server';
|
|
11
12
|
|
|
@@ -28,9 +29,9 @@ console.log(`[ompchamber] db: ${await getDatabasePath()}`);
|
|
|
28
29
|
const port = Number(Bun.env.PORT) || 3000;
|
|
29
30
|
const host = Bun.env.HOST || 'localhost';
|
|
30
31
|
const mode = Bun.env.NODE_ENV === 'production' ? 'prod' : 'dev';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// argv, not env: see launch-mode.ts — an inherited `OMPCHAMBER_LAUNCH_MODE`
|
|
33
|
+
// would label a `bun run dev` started inside an OMPChamber shell as `daemon`.
|
|
34
|
+
const launchMode = resolveLaunchMode();
|
|
34
35
|
|
|
35
36
|
const lock = await acquirePortLock(port);
|
|
36
37
|
if (!lock) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Bun binary to spawn child processes with.
|
|
8
|
+
*
|
|
9
|
+
* The server is usually itself Bun, so `process.execPath` is the most
|
|
10
|
+
* trustworthy answer — it is the exact runtime this install was started with.
|
|
11
|
+
* A host that is not Bun falls back to a PATH lookup, and finally to the
|
|
12
|
+
* literal `bun`, which lets the child resolve it from its own environment.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveBunBin(): string {
|
|
15
|
+
const execPath = process.execPath;
|
|
16
|
+
if (/(^|[\\/])bun$/.test(execPath)) return execPath;
|
|
17
|
+
return Bun.which('bun') ?? 'bun';
|
|
18
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { launchModeArg, resolveLaunchMode } from '@/server/lib/lifecycle/launch-mode';
|
|
4
|
+
|
|
5
|
+
const ENTRY = '/repo/src/server/index.ts';
|
|
6
|
+
|
|
7
|
+
describe('resolveLaunchMode', () => {
|
|
8
|
+
test('reads the flag the CLI appends to the servers it spawns', () => {
|
|
9
|
+
expect(resolveLaunchMode(['bun', ENTRY, '--ompchamber-server', launchModeArg('daemon')])).toBe('daemon');
|
|
10
|
+
expect(resolveLaunchMode(['bun', ENTRY, '--ompchamber-server', launchModeArg('foreground')])).toBe('foreground');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('treats a server started without the flag as direct', () => {
|
|
14
|
+
// `bun run dev`, `bun run start`, a manual run, an external supervisor.
|
|
15
|
+
expect(resolveLaunchMode(['bun', 'run', '--hot', ENTRY, '--ompchamber-server'])).toBe('direct');
|
|
16
|
+
expect(resolveLaunchMode(['bun', ENTRY, '--ompchamber-server', '--launch-mode=sideways'])).toBe('direct');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('ignores an inherited environment marker', () => {
|
|
20
|
+
// The chamber's terminal panel runs commands with the server's own env, so
|
|
21
|
+
// a `bun run dev` started there inherits `daemon` — argv is the only
|
|
22
|
+
// trustworthy signal, and this test fails if env is ever read again.
|
|
23
|
+
const previous = Bun.env.OMPCHAMBER_LAUNCH_MODE;
|
|
24
|
+
Bun.env.OMPCHAMBER_LAUNCH_MODE = 'daemon';
|
|
25
|
+
try {
|
|
26
|
+
expect(resolveLaunchMode(['bun', ENTRY, '--ompchamber-server'])).toBe('direct');
|
|
27
|
+
} finally {
|
|
28
|
+
if (previous === undefined) delete Bun.env.OMPCHAMBER_LAUNCH_MODE;
|
|
29
|
+
else Bun.env.OMPCHAMBER_LAUNCH_MODE = previous;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How this server was launched — the fact that decides whether an update may
|
|
8
|
+
* replace it.
|
|
9
|
+
*
|
|
10
|
+
* The marker travels as an argv flag, never as an environment variable. Env is
|
|
11
|
+
* inherited by every descendant, so a server started from a shell *inside* an
|
|
12
|
+
* OMPChamber tree — the chamber's own terminal panel runs commands with the
|
|
13
|
+
* server's environment — would inherit `daemon` and then be swapped out by the
|
|
14
|
+
* next update, killing a `bun run dev` loop OMPChamber does not own. argv
|
|
15
|
+
* belongs to the process alone.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { InstanceLaunchMode } from '@/server/lib/lifecycle/instance';
|
|
19
|
+
|
|
20
|
+
/** The argv flag the CLI appends to every server it spawns. */
|
|
21
|
+
const LAUNCH_MODE_FLAG = '--launch-mode=';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The argv entry for `mode`. Paired with `resolveLaunchMode`, so the flag
|
|
25
|
+
* spelling exists in exactly one place for the writer and the reader.
|
|
26
|
+
*/
|
|
27
|
+
export function launchModeArg(mode: InstanceLaunchMode): string {
|
|
28
|
+
return `${LAUNCH_MODE_FLAG}${mode}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Launch mode of a server invocation. Only the CLI passes the flag, so a
|
|
33
|
+
* `bun run dev`, a `bun run start`, a manual run and an externally supervised
|
|
34
|
+
* process all resolve to `direct` — the mode that is left alone.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveLaunchMode(argv: readonly string[] = Bun.argv): InstanceLaunchMode {
|
|
37
|
+
const flag = argv.find((arg) => arg.startsWith(LAUNCH_MODE_FLAG));
|
|
38
|
+
const value = flag?.slice(LAUNCH_MODE_FLAG.length);
|
|
39
|
+
return value === 'daemon' || value === 'foreground' ? value : 'direct';
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { isAutoRestartable } from '@/server/lib/lifecycle/restart';
|
|
4
|
+
|
|
5
|
+
// The modal update and `ompchamber update` both decide with this predicate:
|
|
6
|
+
// guessing wrong here kills a `bun run dev` the user owns, or leaves a daemon
|
|
7
|
+
// serving the build that was just replaced.
|
|
8
|
+
describe('isAutoRestartable', () => {
|
|
9
|
+
test('restarts the instances the CLI started', () => {
|
|
10
|
+
expect(isAutoRestartable('daemon')).toBe(true);
|
|
11
|
+
expect(isAutoRestartable('foreground')).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('leaves every instance OMPChamber did not start alone', () => {
|
|
15
|
+
// `bun run dev`, `bun run start`, a manual run, a systemd/pm2 unit.
|
|
16
|
+
expect(isAutoRestartable('direct')).toBe(false);
|
|
17
|
+
// A probed or unreadable record never authorises a kill either.
|
|
18
|
+
expect(isAutoRestartable('unknown')).toBe(false);
|
|
19
|
+
expect(isAutoRestartable(undefined)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Replacing the instance that is serving an update request.
|
|
8
|
+
*
|
|
9
|
+
* A server cannot restart itself: the process holding the port has to exit
|
|
10
|
+
* before a replacement can bind it, and the update response has to leave first.
|
|
11
|
+
* So once the files on disk are replaced, the work goes to a detached
|
|
12
|
+
* `ompchamber restart --port <port>` — the same stop-then-serve the CLI runs
|
|
13
|
+
* after `ompchamber update`.
|
|
14
|
+
*
|
|
15
|
+
* Instances the CLI did not start are left alone. `bun run dev` and
|
|
16
|
+
* `bun run start` execute this package's source in the user's own shell, and an
|
|
17
|
+
* external supervisor (systemd, pm2, a container) owns the process it started;
|
|
18
|
+
* all of them record `direct`, and replacing any of them would break a loop
|
|
19
|
+
* OMPChamber does not own — or fight a second daemon for the port. Their own
|
|
20
|
+
* launcher is what brings the new files up.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync } from 'node:fs';
|
|
24
|
+
import { join as joinPath, resolve as resolvePath } from 'node:path';
|
|
25
|
+
|
|
26
|
+
import { resolveBunBin } from '@/server/lib/lifecycle/bun';
|
|
27
|
+
import { listInstanceRecords } from '@/server/lib/lifecycle/instance';
|
|
28
|
+
import { resolveLaunchMode } from '@/server/lib/lifecycle/launch-mode';
|
|
29
|
+
|
|
30
|
+
/** Package root of the running copy: src/server/lib/lifecycle -> four levels up. */
|
|
31
|
+
const PKG_ROOT = resolvePath(import.meta.dir, '..', '..', '..', '..');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Time between answering the update request and stopping the server. The
|
|
35
|
+
* response is a small JSON body on a local listener; this is the margin that
|
|
36
|
+
* keeps the console's toast from dying with the process that sent it.
|
|
37
|
+
*/
|
|
38
|
+
export const RESTART_GRACE_MS = 1_200;
|
|
39
|
+
|
|
40
|
+
export interface SelfRestartPlan {
|
|
41
|
+
/** A detached restart is armed; `message` says so. */
|
|
42
|
+
restarting: boolean;
|
|
43
|
+
/** What to tell the user, either way. */
|
|
44
|
+
message: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True when the CLI owns this instance's lifecycle and may replace it. `direct`
|
|
49
|
+
* is every server OMPChamber did not start: `bun run dev`, `bun run start`, a
|
|
50
|
+
* manual `bun src/server/index.ts`, or an external supervisor.
|
|
51
|
+
*/
|
|
52
|
+
export function isAutoRestartable(launchMode: string | null | undefined): boolean {
|
|
53
|
+
return launchMode === 'daemon' || launchMode === 'foreground';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One-line reason an instance is left running, shared by the CLI and console. */
|
|
57
|
+
export function skipRestartNote(launchMode: string | null | undefined): string {
|
|
58
|
+
if (launchMode === 'direct') {
|
|
59
|
+
return 'this instance runs from source (`bun run dev` / `bun run start`) — start it again to apply the update';
|
|
60
|
+
}
|
|
61
|
+
return 'this instance is not managed by the ompchamber CLI — restart it yourself to apply the update';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The record this process wrote for itself; falls back to the env it was launched with. */
|
|
65
|
+
function ownInstance(): { port: number; launchMode: string } | null {
|
|
66
|
+
const record = listInstanceRecords().find((entry) => entry.pid === process.pid);
|
|
67
|
+
if (record) return { port: record.port, launchMode: record.launchMode };
|
|
68
|
+
|
|
69
|
+
const port = Number(Bun.env.PORT);
|
|
70
|
+
if (!Number.isFinite(port) || port <= 0) return null;
|
|
71
|
+
return { port, launchMode: resolveLaunchMode() };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `ompchamber restart --port <port>` for the install this server runs from. */
|
|
75
|
+
function restartCommand(port: number): string[] | null {
|
|
76
|
+
const cli = joinPath(PKG_ROOT, 'src', 'cli', 'ompchamber.js');
|
|
77
|
+
if (!existsSync(cli)) return null;
|
|
78
|
+
return [resolveBunBin(), cli, 'restart', '--port', String(port)];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stop and start this port again, once the response has been delivered. The
|
|
83
|
+
* helper is detached so this process exiting does not take it down, and it
|
|
84
|
+
* removes this port's record itself (`stopInstance`) before serving.
|
|
85
|
+
*/
|
|
86
|
+
export function scheduleSelfRestart(): SelfRestartPlan {
|
|
87
|
+
const self = ownInstance();
|
|
88
|
+
if (!self) {
|
|
89
|
+
return { restarting: false, message: 'This instance could not be identified — restart it yourself to apply the update.' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!isAutoRestartable(self.launchMode)) {
|
|
93
|
+
return { restarting: false, message: skipRestartNote(self.launchMode) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const cmd = restartCommand(self.port);
|
|
97
|
+
if (!cmd) {
|
|
98
|
+
return { restarting: false, message: `The ompchamber CLI is missing from ${PKG_ROOT} — restart it yourself to apply the update.` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
console.log(`[ompchamber] restarting on port ${self.port}: ${cmd.join(' ')}`);
|
|
103
|
+
try {
|
|
104
|
+
Bun.spawn({ cmd, cwd: PKG_ROOT, stdin: 'ignore', stdout: 'inherit', stderr: 'inherit', detached: true }).unref();
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error(`[ompchamber] restart helper could not be started: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
}
|
|
108
|
+
}, RESTART_GRACE_MS);
|
|
109
|
+
|
|
110
|
+
return { restarting: true, message: `Restarting OMPChamber on port ${self.port} to apply it — the console reconnects on its own.` };
|
|
111
|
+
}
|
|
@@ -102,10 +102,21 @@ export class AgentSessionWrapper {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/** Anything in flight that a process reset would destroy: the current turn,
|
|
105
|
-
* a compaction, a shell command,
|
|
106
|
-
*
|
|
105
|
+
* a compaction, a shell command, live subagents — which outlive the turn that
|
|
106
|
+
* spawned them, so no other flag here can see them — or an ask/approval dialog
|
|
107
|
+
* omp is BLOCKED on.
|
|
108
|
+
*
|
|
109
|
+
* The dialogs are the load-bearing entry: an ask parks the tool call for as
|
|
110
|
+
* long as the user takes to answer, and omp answers RPC handlers one at a
|
|
111
|
+
* time, so every probe (`get_state`) queues behind it and times out. Without
|
|
112
|
+
* this the timeout path read the session as "idle and unresponsive", killed
|
|
113
|
+
* the child, and with it the question the user was still reading. */
|
|
107
114
|
isBusy(): boolean {
|
|
108
|
-
return
|
|
115
|
+
return (
|
|
116
|
+
this.isRunning() ||
|
|
117
|
+
this.pendingUiDialogs.list().length > 0 ||
|
|
118
|
+
this.subagents.liveCount(Date.now(), SUBAGENT_STALE_MS) > 0
|
|
119
|
+
);
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
start(): void {
|
|
@@ -13,7 +13,7 @@ import { RpcCommandTimeoutError, type RpcProcess } from '@/server/lib/omp/rpc/pr
|
|
|
13
13
|
import { AWAITING_AGENT_START_TIMEOUT_MS, GET_STATE_TIMEOUT_MS, IMAGE_BEARING_COMMANDS, PASSTHROUGH_COMMANDS, PROMPT_ACK_TIMEOUT_MS, RESTARTING_MESSAGE, SESSION_BUSY_MESSAGE, WebRpcError, toImageContents, type AgentEvent, type RpcSessionState, validateAgentImages } from '@/server/lib/omp/rpc/constants';
|
|
14
14
|
import { clearSessionFileCaches } from '@/server/lib/omp/session/files';
|
|
15
15
|
import { notifyRunningChange } from '@/server/lib/omp/rpc/session-registry';
|
|
16
|
-
import { markStreamStatus } from '@/shared/lib/omp/session/stream-state.server';
|
|
16
|
+
import { clearStreamStatus, markStreamStatus } from '@/shared/lib/omp/session/stream-state.server';
|
|
17
17
|
import { buildWebState, type WebStateHost } from '@/server/lib/omp/rpc/web-state';
|
|
18
18
|
|
|
19
19
|
/** Runtime surface AgentSessionWrapper exposes to the command dispatcher. */
|
|
@@ -66,12 +66,24 @@ export async function dispatchSessionCommand(host: SessionCommandHost, command:
|
|
|
66
66
|
throw new Error('Cannot send a prompt while a shell command is running');
|
|
67
67
|
}
|
|
68
68
|
const streamingBehavior = command.streamingBehavior as 'steer' | 'followUp' | undefined;
|
|
69
|
+
// The live `stream` row starts with the DISPATCH, not with agent_start:
|
|
70
|
+
// the spawn/ack round trip that precedes agent_start must not read as
|
|
71
|
+
// "nothing happened" in the sidebar after the user hit send. Skipped
|
|
72
|
+
// while a turn already streams — that row belongs to the running turn,
|
|
73
|
+
// and this dispatch's failure must never roll it back.
|
|
74
|
+
const ownsStreamRow = !streamingBehavior && !host.streaming && Boolean(host.sessionId);
|
|
75
|
+
// A prompt that never started a turn leaves no run behind, so the row we
|
|
76
|
+
// wrote must go — unless a turn began meanwhile, which owns it now.
|
|
77
|
+
const releaseStreamRow = (): void => {
|
|
78
|
+
if (ownsStreamRow && !host.streaming) void clearStreamStatus(host.sessionId);
|
|
79
|
+
};
|
|
69
80
|
if (!streamingBehavior) {
|
|
70
81
|
host.promptRunning = true;
|
|
71
82
|
host.promptDispatchPendingCount += 1;
|
|
72
83
|
host.awaitingAgentStart = false;
|
|
73
84
|
host.awaitingAgentStartDeadline = 0;
|
|
74
85
|
host.continuationGraceUntil = 0;
|
|
86
|
+
if (ownsStreamRow) void markStreamStatus(host.sessionId, 'stream');
|
|
75
87
|
notifyRunningChange();
|
|
76
88
|
}
|
|
77
89
|
try {
|
|
@@ -85,6 +97,7 @@ export async function dispatchSessionCommand(host: SessionCommandHost, command:
|
|
|
85
97
|
host.promptRunning = false;
|
|
86
98
|
host.awaitingAgentStart = false;
|
|
87
99
|
host.awaitingAgentStartDeadline = 0;
|
|
100
|
+
releaseStreamRow();
|
|
88
101
|
host.emit({ type: 'prompt_result', agentInvoked: false });
|
|
89
102
|
notifyRunningChange();
|
|
90
103
|
} else if (!streamingBehavior && ack?.agentInvoked !== false) {
|
|
@@ -95,6 +108,7 @@ export async function dispatchSessionCommand(host: SessionCommandHost, command:
|
|
|
95
108
|
host.promptRunning = false;
|
|
96
109
|
host.awaitingAgentStart = false;
|
|
97
110
|
host.awaitingAgentStartDeadline = 0;
|
|
111
|
+
releaseStreamRow();
|
|
98
112
|
notifyRunningChange();
|
|
99
113
|
// The ack may simply be queued behind a running turn: omp accepts the
|
|
100
114
|
// prompt before the turn it starts. No reset, and the client must not
|
|
@@ -72,6 +72,24 @@ export function getRunningRpcSessionIds(): string[] {
|
|
|
72
72
|
return getRunningRpcSessions().map((s) => s.id);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Sessions whose omp process is parked on a dialog only the user can release —
|
|
77
|
+
* an `ask` question or an approval gate. Deliberately independent of
|
|
78
|
+
* `isRunning()`: a blocked dialog leaves every turn flag exactly as it was, so
|
|
79
|
+
* the pending-dialog registry is the only thing that can tell a session waiting
|
|
80
|
+
* for an answer from an idle one. Feeds the sidebar's "needs input" badge, which
|
|
81
|
+
* has to reach clients that never opened the session.
|
|
82
|
+
*/
|
|
83
|
+
export function getAwaitingInputSessionIds(): string[] {
|
|
84
|
+
const ids = new Set<string>();
|
|
85
|
+
for (const [sessionId, session] of getRegistry()) {
|
|
86
|
+
if (session.isAlive() && session.getPendingUiDialogs().length > 0) {
|
|
87
|
+
ids.add(session.sessionId || sessionId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return [...ids];
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
function getRunningListeners(): Set<(update: RunningSessionUpdate) => void> {
|
|
76
94
|
if (!globalThis.__ompRunningListeners) globalThis.__ompRunningListeners = new Set();
|
|
77
95
|
return globalThis.__ompRunningListeners;
|
|
@@ -22,7 +22,7 @@ import { isValidSessionSortOption, sortFolders } from '@/shared/lib/workspace/si
|
|
|
22
22
|
import { loadOmpSidebarData } from '@/server/lib/omp/session/reader';
|
|
23
23
|
import { sessionHasSubagents } from '@/server/lib/omp/session/subagent-presence';
|
|
24
24
|
import { healStaleStreamStatuses, loadStreamStatuses } from '@/shared/lib/omp/session/stream-state.server';
|
|
25
|
-
import { getRunningRpcSessionIds } from '@/server/lib/omp/rpc/session-registry';
|
|
25
|
+
import { getAwaitingInputSessionIds, getRunningRpcSessionIds } from '@/server/lib/omp/rpc/session-registry';
|
|
26
26
|
import type { SessionItemData, SessionSortOption, WorkspaceFolderData } from '@/shared/types';
|
|
27
27
|
import type { OmpSession } from '@/shared/types/omp/session';
|
|
28
28
|
|
|
@@ -115,15 +115,21 @@ export async function loadSidebarData(): Promise<SessionListPayload> {
|
|
|
115
115
|
// `finish` right here — the authoritative status travels with the same
|
|
116
116
|
// fetch that refreshes the sidebar list.
|
|
117
117
|
const streamStatuses: Record<string, 'stream' | 'finish' | 'abort' | 'error'> = {};
|
|
118
|
+
// Sessions blocked on a dialog nobody has answered yet. Read live from the
|
|
119
|
+
// process registry (never persisted): the child that owns the question is the
|
|
120
|
+
// same thing that owns the flag, so a restart cannot leave a stale badge.
|
|
121
|
+
const awaitingInput = new Set<string>();
|
|
118
122
|
if (!mock) {
|
|
119
123
|
await healStaleStreamStatuses(new Set(getRunningRpcSessionIds()));
|
|
120
124
|
Object.assign(streamStatuses, await loadStreamStatuses());
|
|
125
|
+
for (const id of getAwaitingInputSessionIds()) awaitingInput.add(id);
|
|
121
126
|
}
|
|
122
127
|
const foldersWithStatus = groupedFolders.map((folder) => ({
|
|
123
128
|
...folder,
|
|
124
129
|
sessions: (folder.sessions ?? []).map((s) => ({
|
|
125
130
|
...s,
|
|
126
131
|
streamStatus: streamStatuses[String(s.id)],
|
|
132
|
+
...(awaitingInput.has(String(s.id)) ? { awaitingInput: true } : {}),
|
|
127
133
|
})),
|
|
128
134
|
}));
|
|
129
135
|
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Applies a requested update. oh-my-pi is updated through the omp binary;
|
|
3
3
|
* OMPChamber through the shared self-update engine, which replaces the running
|
|
4
|
-
* install in place (bun global install, or git fast-forward + rebuild)
|
|
4
|
+
* install in place (bun global install, or git fast-forward + rebuild) and then
|
|
5
|
+
* restarts this instance through a detached `ompchamber restart` — unless the
|
|
6
|
+
* instance was started from source, which its own launcher has to restart.
|
|
5
7
|
*/
|
|
6
8
|
|
|
9
|
+
import { scheduleSelfRestart } from '@/server/lib/lifecycle/restart';
|
|
7
10
|
import { updateOmpChamber } from '@/server/lib/updates/install';
|
|
8
11
|
import { applyOmpUpdate } from '@/server/lib/updates/omp';
|
|
9
12
|
import type { UpdateApplyResult, UpdateTarget } from '@/shared/types/updates';
|
|
10
13
|
|
|
11
|
-
/** A server cannot replace its own process, so the user restarts it. */
|
|
12
|
-
const RESTART_NOTE = ' Restart the server (`ompchamber restart`) to apply it.';
|
|
13
|
-
|
|
14
14
|
export async function applyUpdate(target: UpdateTarget): Promise<UpdateApplyResult> {
|
|
15
15
|
if (target === 'omp') {
|
|
16
16
|
try {
|
|
@@ -28,11 +28,14 @@ export async function applyUpdate(target: UpdateTarget): Promise<UpdateApplyResu
|
|
|
28
28
|
|
|
29
29
|
if (target === 'ompchamber') {
|
|
30
30
|
const result = await updateOmpChamber();
|
|
31
|
+
// Only a call that actually replaced files has something to restart for:
|
|
32
|
+
// an "already up to date" answer must not bounce a healthy server.
|
|
33
|
+
const restart = result.success && result.updated ? scheduleSelfRestart() : null;
|
|
31
34
|
return {
|
|
32
35
|
success: result.success,
|
|
33
36
|
target: 'ompchamber',
|
|
34
37
|
manual: result.manual,
|
|
35
|
-
message: `${result.message}${
|
|
38
|
+
message: restart ? `${result.message} ${restart.message}` : result.message,
|
|
36
39
|
output: result.output || undefined,
|
|
37
40
|
};
|
|
38
41
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { join as joinPath, resolve as resolvePath } from 'node:path';
|
|
16
16
|
|
|
17
|
+
import { resolveBunBin } from '@/server/lib/lifecycle/bun';
|
|
17
18
|
import { fetchLatestRelease, type GitHubRelease } from '@/server/lib/updates/github';
|
|
18
19
|
import {
|
|
19
20
|
detectInstallMethod,
|
|
@@ -137,10 +138,12 @@ export function resolveInstallContext(pkgRoot = DEFAULT_PKG_ROOT): InstallContex
|
|
|
137
138
|
const remote = gitWorkTree ? runSync(['git', '-C', root, 'remote', 'get-url', 'origin']) : null;
|
|
138
139
|
const gitRemote = remote?.ok && remote.out.length > 0 ? remote.out : null;
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
return {
|
|
142
|
+
...detectInstallMethod({ pkgRoot: root, gitWorkTree, gitRemote }),
|
|
143
|
+
pkgRoot: root,
|
|
144
|
+
gitRemote,
|
|
145
|
+
bunBin: resolveBunBin(),
|
|
146
|
+
};
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
/** The commands that install `version` for a given install method. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { json, NO_STORE_HEADERS } from '@/server/lib/remix-compat';
|
|
2
2
|
import pkg from '@/../package.json';
|
|
3
|
+
import { resolveLaunchMode } from '@/server/lib/lifecycle/launch-mode';
|
|
3
4
|
import { isMockMode } from '@/server/mock.server';
|
|
4
5
|
|
|
5
6
|
const STARTED_AT = Date.now();
|
|
@@ -26,7 +27,7 @@ export async function loader() {
|
|
|
26
27
|
// `launchMode` are what `status` prints, and the pair with `pid` is what
|
|
27
28
|
// separates a dev server from a stale published build.
|
|
28
29
|
mode: Bun.env.NODE_ENV === 'production' ? 'prod' : 'dev',
|
|
29
|
-
launchMode:
|
|
30
|
+
launchMode: resolveLaunchMode(),
|
|
30
31
|
port: Number(Bun.env.PORT) || 3000,
|
|
31
32
|
host: Bun.env.HOST || 'localhost',
|
|
32
33
|
},
|