ompchamber 3.1.0 → 3.2.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 +6 -3
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +279 -280
- package/dist/client/static/js/index-01dv383b.css +1 -0
- package/dist/client/static/js/{index-8t70jhyj.js → index-1q6165r3.js} +641 -641
- package/package.json +1 -1
- package/src/cli/lib/args.js +4 -3
- package/src/cli/lib/commands/restart.js +7 -6
- package/src/cli/lib/commands/stop.js +38 -31
- package/src/cli/lib/commands/update.js +3 -2
- package/src/cli/lib/output.js +15 -8
- package/src/cli/lib/stop-scope.js +23 -0
- package/src/cli/lib/stop-scope.test.js +41 -0
- package/src/client/components/common/Modal.tsx +14 -4
- package/src/client/components/{layout/session-sidebar/SubagentList.tsx → common/subagent-list/index.tsx} +8 -10
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +6 -30
- package/src/client/components/layout/session-sidebar/Toolbar.tsx +11 -5
- package/src/client/components/layout/session-sidebar/index.tsx +4 -2
- package/src/client/components/mobile/LayoutWrapper.tsx +6 -0
- package/src/client/components/mobile/SessionSidebar.tsx +4 -0
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +66 -29
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +50 -12
- package/src/client/components/mobile/mobile-session-sidebar/Toolbar.tsx +21 -1
- package/src/client/components/settings/Modal.tsx +35 -6
- package/src/client/components/settings/SettingsSidebarList.tsx +1 -1
- package/src/client/components/settings/categories/AgentSettings.tsx +45 -28
- package/src/client/components/settings/categories/CommandSettings.tsx +45 -28
- package/src/client/components/settings/categories/McpSettings.tsx +48 -31
- package/src/client/components/settings/categories/ProjectSettings.tsx +34 -19
- package/src/client/components/settings/categories/ProviderSettings.tsx +77 -53
- package/src/client/components/settings/categories/SkillSettings.tsx +34 -17
- package/src/client/components/settings/categories/UsageSettings.tsx +53 -39
- package/src/client/components/settings/categories/behavior-settings/Editor.tsx +7 -7
- package/src/client/components/settings/categories/mcp-settings/ImportModal.tsx +1 -1
- package/src/client/components/settings/categories/mcp-settings/SidebarList.tsx +1 -1
- package/src/client/components/settings/categories/project-settings/SidebarList.tsx +1 -1
- package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +8 -3
- package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +1 -1
- package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +9 -4
- package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +1 -1
- package/src/client/components/settings/categories/skill-catalog/AddSourceModal.tsx +1 -1
- package/src/client/components/settings/categories/skill-settings/SidebarList.tsx +1 -1
- package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +1 -1
- package/src/client/components/settings/master-detail/index.tsx +68 -0
- package/src/client/components/workspace/SearchPanel.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +7 -1
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/index.tsx +1 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +2 -1
- package/src/client/components/workspace/diff-panel/SplitView.tsx +31 -5
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +77 -6
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +18 -5
- package/src/client/components/workspace/diff-panel/index.tsx +36 -7
- package/src/client/components/workspace/editor/Toolbar.tsx +11 -4
- package/src/client/components/workspace/git-panel/commit-modal/CommitDiffViewer.tsx +2 -1
- package/src/client/hooks/chat/omp/session-list.tsx +31 -1
- package/src/client/hooks/chat/omp/session-sidebar-controller.ts +7 -1
- package/src/client/hooks/chat/subagent/index.ts +37 -11
- package/src/client/hooks/editor/use-file-editor.ts +1 -1
- package/src/client/hooks/settings/master-detail.ts +33 -0
- package/src/client/hooks/workspace/expanded-sessions.ts +62 -0
- package/src/server/lib/bundler/css.test.ts +87 -0
- package/src/server/lib/bundler/css.ts +17 -2
- package/src/server/lib/bundler/nested-at-rules.test.ts +100 -0
- package/src/server/lib/bundler/nested-at-rules.ts +146 -0
- package/src/server/lib/db/client.ts +5 -1
- package/src/server/lib/fs/git-diff.ts +34 -10
- package/src/server/lib/fs/git-repos.ts +81 -0
- package/src/server/lib/lifecycle/flock.ts +199 -0
- package/src/server/lib/lifecycle/identity.ts +5 -1
- package/src/server/lib/lifecycle/launch-mode.test.ts +20 -1
- package/src/server/lib/lifecycle/launch-mode.ts +26 -0
- package/src/server/lib/lifecycle/port-guard.ts +18 -3
- package/src/server/lib/lifecycle/proc/darwin.ts +52 -17
- package/src/server/lib/lifecycle/proc/linux.ts +29 -7
- package/src/server/lib/lifecycle/proc/ps.ts +7 -0
- package/src/server/lib/lifecycle/proc/types.ts +17 -0
- package/src/server/lib/lifecycle/proc/win32.ts +5 -0
- package/src/server/lib/lifecycle/restart.ts +9 -14
- package/src/server/lib/observer-ws.ts +150 -0
- package/src/server/lib/omp/rpc/constants.ts +9 -0
- package/src/server/lib/omp/rpc/subagent-liveness.ts +2 -12
- package/src/server/lib/omp/session/file-scan.ts +7 -1
- package/src/server/lib/omp/session/files.ts +95 -96
- package/src/server/lib/omp/session/locator.ts +18 -8
- package/src/server/lib/omp/session/messages.ts +7 -7
- package/src/server/lib/omp/session/scan.ts +108 -0
- package/src/server/lib/omp/session/stats.ts +50 -50
- package/src/server/lib/omp/session/telemetry/scan.ts +89 -7
- package/src/server/lib/omp/session/telemetry-raw.ts +24 -8
- package/src/server/lib/omp/session/usage/aggregate.ts +170 -55
- package/src/server/lib/terminal/runtime.server.ts +3 -0
- package/src/server/lib/terminal/session.server.ts +30 -3
- package/src/server/routes/agent/command.test.ts +69 -0
- package/src/server/routes/agent/command.ts +14 -0
- package/src/server/routes/agent/ws.test.ts +159 -0
- package/src/server/routes/agent/ws.ts +22 -97
- package/src/server/routes/btw/ws.ts +22 -95
- package/src/server/routes/fs/git.ts +61 -70
- package/src/server/routes/omp/session-stats.ts +1 -1
- package/src/server/routes/telemetry/tokens.ts +6 -4
- package/src/server/routes/terminal/run.ts +4 -8
- package/src/server/routes/terminal/ws.ts +24 -2
- package/src/shared/lib/chat/omp/agent-events.test.ts +33 -1
- package/src/shared/lib/chat/omp/agent-events.ts +10 -0
- package/src/shared/lib/code/language.ts +146 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +2 -1
- package/src/shared/lib/code/syntax-highlight.ts +0 -122
- package/src/shared/lib/fs/diff-parser.test.ts +5 -5
- package/src/shared/lib/fs/diff-parser.ts +67 -42
- package/src/shared/lib/omp/subagent/parse.test.ts +88 -0
- package/src/shared/lib/omp/subagent/parse.ts +17 -0
- package/src/shared/lib/omp/subagent/transcript-client.test.ts +100 -0
- package/src/shared/lib/omp/subagent/transcript-client.ts +17 -1
- package/src/shared/types/omp/subagent.ts +6 -0
- package/dist/client/static/js/index-n4wkqj7t.css +0 -1
- package/src/server/lib/lifecycle/restart.test.ts +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ompchamber",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Developer web console and diagnostic chamber for AI Oh-My-Pi — Elysia + Preact on Bun.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Wijanarko Putra Rajeb <wijanarko.rajeb@gmail.com>",
|
package/src/cli/lib/args.js
CHANGED
|
@@ -188,7 +188,7 @@ USAGE:
|
|
|
188
188
|
COMMANDS:
|
|
189
189
|
serve Start the OMPChamber web server (default when no command is given)
|
|
190
190
|
update Update OMPChamber to the latest GitHub release
|
|
191
|
-
stop Stop
|
|
191
|
+
stop Stop the instances the CLI started (--port/--all to stop any)
|
|
192
192
|
restart Stop and start again (every CLI-started instance, or --port <port>)
|
|
193
193
|
status Show running instances (every one, or --port <port>)
|
|
194
194
|
logs Print or follow server logs (every instance, or --port <port>)
|
|
@@ -201,7 +201,7 @@ OPTIONS:
|
|
|
201
201
|
--prod Run the production build instead of the dev server
|
|
202
202
|
--foreground Run the server in the foreground (no daemon)
|
|
203
203
|
--no-daemon Alias for --foreground
|
|
204
|
-
--all
|
|
204
|
+
--all Every instance, including ones started outside the CLI
|
|
205
205
|
-c, --check Report whether a newer release exists without installing it
|
|
206
206
|
--force Reinstall even when already up to date
|
|
207
207
|
--no-restart Do not restart a running instance after updating
|
|
@@ -226,7 +226,8 @@ EXAMPLES:
|
|
|
226
226
|
ompchamber serve --prod # Serve the production build
|
|
227
227
|
ompchamber update --check # Is a newer release available?
|
|
228
228
|
ompchamber update # Update, then restart a running instance
|
|
229
|
-
ompchamber stop # Stop the
|
|
229
|
+
ompchamber stop # Stop the instances the CLI started
|
|
230
|
+
ompchamber stop --port 3000 # Stop the instance on port 3000, whoever started it
|
|
230
231
|
ompchamber restart # Restart the server
|
|
231
232
|
ompchamber status # Show whether the server is running
|
|
232
233
|
ompchamber logs -f -n 200 # Follow the last 200 log lines
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
// `ompchamber restart` — stop live instances, then serve again.
|
|
2
2
|
//
|
|
3
3
|
// Scoping matches `status` and `stop`: without `--port` every live instance is
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// considered, and `--port <port>` restricts the restart to that one. With
|
|
5
|
+
// nothing running, the command is simply `serve`.
|
|
6
6
|
//
|
|
7
7
|
// Instances the CLI did not start are left alone, exactly as `ompchamber update`
|
|
8
8
|
// leaves them: `bun run dev` and `bun run start` own their own process, so
|
|
9
|
-
// replacing it would break a loop OMPChamber does not manage. `ompchamber stop
|
|
10
|
-
// is
|
|
9
|
+
// replacing it would break a loop OMPChamber does not manage. `ompchamber stop
|
|
10
|
+
// --port <port>` (or `--all`) is the deliberate way to end one.
|
|
11
11
|
|
|
12
12
|
import { findLiveInstance, listLiveInstances, stopInstance } from '@/cli/lib/runtime.js';
|
|
13
13
|
import { STOP_TIMEOUT_MS } from '@/cli/lib/process-lifecycle.js';
|
|
14
14
|
import { log, isJson, isQuiet } from '@/cli/lib/output.js';
|
|
15
15
|
import { run as runServe } from '@/cli/lib/commands/serve.js';
|
|
16
|
-
import {
|
|
16
|
+
import { skipRestartNote } from '@/server/lib/lifecycle/restart';
|
|
17
|
+
import { isCliManaged } from '@/server/lib/lifecycle/launch-mode';
|
|
17
18
|
|
|
18
19
|
function explicitPort(options) {
|
|
19
20
|
const raw = options?.port;
|
|
@@ -34,7 +35,7 @@ export async function run(options, ctx) {
|
|
|
34
35
|
if (targets.length === 0) return runServe(options, ctx);
|
|
35
36
|
|
|
36
37
|
for (const live of targets) {
|
|
37
|
-
if (!
|
|
38
|
+
if (!isCliManaged(live.launchMode)) {
|
|
38
39
|
if (!json && !quiet) log(`Skipping OMPChamber on port ${live.port}: ${skipRestartNote(live.launchMode)}.`);
|
|
39
40
|
continue;
|
|
40
41
|
}
|
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
// `ompchamber stop` — stop
|
|
1
|
+
// `ompchamber stop` — stop OMPChamber instances.
|
|
2
|
+
//
|
|
3
|
+
// Scope follows OWNERSHIP, the same rule `restart` and `update` apply. By
|
|
4
|
+
// default only the instances the CLI started are stopped, because a server
|
|
5
|
+
// running from source is supervised by a launcher the CLI does not own and
|
|
6
|
+
// that launcher exits with the child it spawned: `bun run dev` is a process
|
|
7
|
+
// whose whole job is to mirror the script under it, so signalling the server
|
|
8
|
+
// takes the user's dev loop down with it. `--port <port>` and `--all` are the
|
|
9
|
+
// explicit forms that do end one — naming an instance is a deliberate call,
|
|
10
|
+
// not a default.
|
|
2
11
|
|
|
3
12
|
import { listLiveInstances, findLiveInstance, stopInstance } from '@/cli/lib/runtime.js';
|
|
4
13
|
import { STOP_TIMEOUT_MS } from '@/cli/lib/process-lifecycle.js';
|
|
5
|
-
import { ok, warn, printJson, isJson } from '@/cli/lib/output.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const raw = options?.port;
|
|
9
|
-
if (raw !== null && raw !== undefined) {
|
|
10
|
-
const parsed = Number(raw);
|
|
11
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
12
|
-
}
|
|
13
|
-
const env = Bun.env.OMPCHAMBER_PORT;
|
|
14
|
-
if (typeof env === 'string' && env.trim().length > 0) {
|
|
15
|
-
const parsed = Number(env.trim());
|
|
16
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
17
|
-
}
|
|
18
|
-
return 3000;
|
|
19
|
-
}
|
|
14
|
+
import { ok, warn, log, printJson, isJson, isQuiet } from '@/cli/lib/output.js';
|
|
15
|
+
import { unmanagedReason } from '@/server/lib/lifecycle/launch-mode';
|
|
16
|
+
import { partitionStopTargets } from '@/cli/lib/stop-scope.js';
|
|
20
17
|
|
|
21
18
|
function explicitPort(options) {
|
|
22
19
|
const raw = options?.port;
|
|
@@ -25,30 +22,40 @@ function explicitPort(options) {
|
|
|
25
22
|
return Number.isFinite(parsed) ? parsed : null;
|
|
26
23
|
}
|
|
27
24
|
|
|
25
|
+
/** How to end an instance the default scope leaves alone. */
|
|
26
|
+
function stopHint(port) {
|
|
27
|
+
return `stop it with \`ompchamber stop --port ${port}\` or \`ompchamber stop --all\``;
|
|
28
|
+
}
|
|
29
|
+
|
|
28
30
|
export async function run(options) {
|
|
29
31
|
const json = isJson();
|
|
32
|
+
const quiet = isQuiet();
|
|
30
33
|
const requested = explicitPort(options);
|
|
34
|
+
// `--all` is the explicit spelling of "every instance, whoever started it".
|
|
35
|
+
const explicitScope = requested !== null || Boolean(options?.all);
|
|
36
|
+
|
|
37
|
+
const live = requested !== null
|
|
38
|
+
? [await findLiveInstance(requested)].filter(Boolean)
|
|
39
|
+
: await listLiveInstances();
|
|
40
|
+
const { targets, skipped } = partitionStopTargets(live, explicitScope);
|
|
41
|
+
const skippedReport = skipped.map((entry) => ({ port: entry.port, pid: entry.pid, launchMode: entry.launchMode }));
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
let targets;
|
|
37
|
-
if (requested !== null) {
|
|
38
|
-
const live = await findLiveInstance(requested);
|
|
39
|
-
targets = live ? [live] : [];
|
|
40
|
-
} else {
|
|
41
|
-
targets = await listLiveInstances();
|
|
43
|
+
if (!json && !quiet) {
|
|
44
|
+
for (const entry of skipped) {
|
|
45
|
+
log(`Skipping OMPChamber on port ${entry.port} (pid ${entry.pid}): it ${unmanagedReason(entry.launchMode)} — ${stopHint(entry.port)}.`);
|
|
46
|
+
}
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
if (targets.length === 0) {
|
|
45
50
|
if (json) {
|
|
46
|
-
printJson({ stopped: [] });
|
|
51
|
+
printJson({ stopped: [], skipped: skippedReport });
|
|
47
52
|
return;
|
|
48
53
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
if (skipped.length === 0) {
|
|
55
|
+
warn(requested === null
|
|
56
|
+
? 'No running OMPChamber instances found.'
|
|
57
|
+
: `No running OMPChamber instance on port ${requested}.`);
|
|
58
|
+
}
|
|
52
59
|
return;
|
|
53
60
|
}
|
|
54
61
|
|
|
@@ -64,5 +71,5 @@ export async function run(options) {
|
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
if (json) printJson({ stopped });
|
|
74
|
+
if (json) printJson({ stopped, skipped: skippedReport });
|
|
68
75
|
}
|
|
@@ -10,7 +10,8 @@ import { color, configure, error, log, ok, warn, printJson, isJson, isQuiet } fr
|
|
|
10
10
|
import { listLiveInstances, findLiveInstance, stopInstance } from '@/cli/lib/runtime.js';
|
|
11
11
|
import { STOP_TIMEOUT_MS } from '@/cli/lib/process-lifecycle.js';
|
|
12
12
|
import { run as runServe } from '@/cli/lib/commands/serve.js';
|
|
13
|
-
import {
|
|
13
|
+
import { skipRestartNote } from '@/server/lib/lifecycle/restart';
|
|
14
|
+
import { isCliManaged } from '@/server/lib/lifecycle/launch-mode';
|
|
14
15
|
import { resolveInstallContext, resolveOmpChamberVersion, updateOmpChamber } from '@/server/lib/updates/install';
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -30,7 +31,7 @@ async function restartInstances(options, ctx) {
|
|
|
30
31
|
const restarted = [];
|
|
31
32
|
const skipped = [];
|
|
32
33
|
for (const entry of await liveInstances(options)) {
|
|
33
|
-
if (!
|
|
34
|
+
if (!isCliManaged(entry.launchMode)) {
|
|
34
35
|
skipped.push({ port: entry.port, pid: entry.pid, launchMode: entry.launchMode });
|
|
35
36
|
log(`Skipping OMPChamber on port ${entry.port}: ${skipRestartNote(entry.launchMode)}.`);
|
|
36
37
|
continue;
|
package/src/cli/lib/output.js
CHANGED
|
@@ -95,6 +95,12 @@ export function fail(message, code = 1) {
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Render a simple padded plain-text table for human output.
|
|
98
|
+
*
|
|
99
|
+
* Widths are measured with `Bun.stringWidth`, not `String.length`: `.length`
|
|
100
|
+
* counts UTF-16 code units, so a CJK session title or an emoji reported a width
|
|
101
|
+
* it does not render at and the whole table misaligned (verified: a 5-column
|
|
102
|
+
* overcount on a Japanese title). `stringWidth` also ignores ANSI escapes, so a
|
|
103
|
+
* coloured cell does not pad against its own escape bytes.
|
|
98
104
|
*/
|
|
99
105
|
export function formatTable(rows, headers = []) {
|
|
100
106
|
const body = Array.isArray(rows) ? rows.map((row) => (Array.isArray(row) ? row : [row])) : [];
|
|
@@ -105,21 +111,22 @@ export function formatTable(rows, headers = []) {
|
|
|
105
111
|
return '';
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
const cellText = (row, i) => (row[i] === undefined || row[i] === null ? '' : String(row[i]));
|
|
108
115
|
const widths = [];
|
|
109
116
|
for (let i = 0; i < columns; i += 1) {
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
widths[i] =
|
|
117
|
+
let width = head[i] === undefined ? 0 : Bun.stringWidth(String(head[i]));
|
|
118
|
+
for (const row of body) {
|
|
119
|
+
const cellWidth = Bun.stringWidth(cellText(row, i));
|
|
120
|
+
if (cellWidth > width) width = cellWidth;
|
|
121
|
+
}
|
|
122
|
+
widths[i] = width;
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
const renderRow = (row) =>
|
|
119
126
|
widths
|
|
120
127
|
.map((width, i) => {
|
|
121
|
-
const cell = row
|
|
122
|
-
return cell.
|
|
128
|
+
const cell = cellText(row, i);
|
|
129
|
+
return cell + ' '.repeat(Math.max(0, width - Bun.stringWidth(cell)));
|
|
123
130
|
})
|
|
124
131
|
.join(' ')
|
|
125
132
|
.trimEnd();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Ownership scope for `ompchamber stop`.
|
|
2
|
+
//
|
|
3
|
+
// Kept separate from the command so the rule is testable without a live
|
|
4
|
+
// server: the default scope is the instances the CLI started, and only an
|
|
5
|
+
// explicit `--port <port>` / `--all` widens it to the ones it did not. That
|
|
6
|
+
// distinction is the whole reason this module exists — a server running from
|
|
7
|
+
// source sits under a launcher that exits with its child, so stopping it by
|
|
8
|
+
// default takes a `bun run dev` loop down with it.
|
|
9
|
+
|
|
10
|
+
import { isCliManaged } from '@/server/lib/lifecycle/launch-mode';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Split the live instances into the ones `stop` acts on and the ones it leaves
|
|
14
|
+
* alone. `explicit` is true when the user named a port or passed `--all`:
|
|
15
|
+
* naming an instance is a deliberate call, so ownership no longer gates it.
|
|
16
|
+
*/
|
|
17
|
+
export function partitionStopTargets(instances, explicit) {
|
|
18
|
+
if (explicit) return { targets: instances, skipped: [] };
|
|
19
|
+
return {
|
|
20
|
+
targets: instances.filter((entry) => isCliManaged(entry.launchMode)),
|
|
21
|
+
skipped: instances.filter((entry) => !isCliManaged(entry.launchMode)),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
|
|
8
|
+
import { partitionStopTargets } from '@/cli/lib/stop-scope.js';
|
|
9
|
+
|
|
10
|
+
const daemon = { port: 3000, pid: 11, launchMode: 'daemon' };
|
|
11
|
+
const foreground = { port: 3001, pid: 12, launchMode: 'foreground' };
|
|
12
|
+
const dev = { port: 3002, pid: 13, launchMode: 'direct' };
|
|
13
|
+
const unknown = { port: 3003, pid: 14, launchMode: 'unknown' };
|
|
14
|
+
|
|
15
|
+
describe('partitionStopTargets', () => {
|
|
16
|
+
// The default scope is ownership, not "everything that answers /api/health".
|
|
17
|
+
// `bun run dev` is a supervisor whose only job is to mirror the script under
|
|
18
|
+
// it, so stopping that server exits the user's dev loop too — the bug this
|
|
19
|
+
// rule exists to prevent.
|
|
20
|
+
test('stops only the instances the CLI started when the scope is implicit', () => {
|
|
21
|
+
const { targets, skipped } = partitionStopTargets([daemon, dev, foreground, unknown], false);
|
|
22
|
+
expect(targets).toEqual([daemon, foreground]);
|
|
23
|
+
expect(skipped).toEqual([dev, unknown]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Naming an instance — a port or `--all` — is a deliberate call, so the
|
|
27
|
+
// ownership rule no longer gates it.
|
|
28
|
+
test('widens to every instance when the scope is explicit', () => {
|
|
29
|
+
const { targets, skipped } = partitionStopTargets([daemon, dev, unknown], true);
|
|
30
|
+
expect(targets).toEqual([daemon, dev, unknown]);
|
|
31
|
+
expect(skipped).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('an unreadable launch mode is never stopped implicitly', () => {
|
|
35
|
+
// A record whose launchMode could not be read must not authorise a kill:
|
|
36
|
+
// the safe reading is "not ours".
|
|
37
|
+
const { targets, skipped } = partitionStopTargets([{ port: 4000, pid: 99, launchMode: undefined }], false);
|
|
38
|
+
expect(targets).toEqual([]);
|
|
39
|
+
expect(skipped.length).toBe(1);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -45,13 +45,19 @@ export function Modal({
|
|
|
45
45
|
return (
|
|
46
46
|
<div
|
|
47
47
|
className={`fixed inset-0 bg-ink/40 backdrop-blur-[2px] ${zClass} flex items-center justify-center p-4`}
|
|
48
|
+
style={{
|
|
49
|
+
paddingTop: 'max(1rem, env(safe-area-inset-top, 0px))',
|
|
50
|
+
paddingBottom: 'max(1rem, env(safe-area-inset-bottom, 0px))',
|
|
51
|
+
paddingLeft: 'max(1rem, env(safe-area-inset-left, 0px))',
|
|
52
|
+
paddingRight: 'max(1rem, env(safe-area-inset-right, 0px))',
|
|
53
|
+
}}
|
|
48
54
|
onClick={onClose}
|
|
49
55
|
>
|
|
50
56
|
<div
|
|
51
|
-
className={`bg-paper border border-ink/15 rounded-xl shadow-2xl w-full ${maxWidthClass} overflow-hidden text-ink`}
|
|
57
|
+
className={`bg-paper border border-ink/15 rounded-xl shadow-2xl w-full ${maxWidthClass} max-h-full flex flex-col overflow-hidden text-ink`}
|
|
52
58
|
onClick={(e) => e.stopPropagation()}
|
|
53
59
|
>
|
|
54
|
-
<div className="px-5 py-4 border-b border-ink/10 flex items-center justify-between">
|
|
60
|
+
<div className="px-5 py-4 border-b border-ink/10 flex items-center justify-between flex-shrink-0">
|
|
55
61
|
{header}
|
|
56
62
|
<button
|
|
57
63
|
type="button"
|
|
@@ -63,11 +69,15 @@ export function Modal({
|
|
|
63
69
|
</div>
|
|
64
70
|
|
|
65
71
|
{form ? (
|
|
66
|
-
|
|
72
|
+
// The body scrolls, not the panel: a long form (the add-provider
|
|
73
|
+
// dialog is 832px of fields) is taller than a phone viewport, and
|
|
74
|
+
// without a bounded, scrollable body its footer sat below the screen
|
|
75
|
+
// with no way to reach it — the dialog could not be submitted.
|
|
76
|
+
<form onSubmit={form.onSubmit} className={`${form.className} min-h-0 overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static`}>
|
|
67
77
|
{body}
|
|
68
78
|
</form>
|
|
69
79
|
) : (
|
|
70
|
-
body
|
|
80
|
+
<div className="min-h-0 overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static">{body}</div>
|
|
71
81
|
)}
|
|
72
82
|
</div>
|
|
73
83
|
</div>
|
|
@@ -4,7 +4,7 @@ import { SubagentStatusIcon } from '@/client/components/common/SubagentStatusIco
|
|
|
4
4
|
import { isRecord } from '@/shared/lib/util/guards';
|
|
5
5
|
import { fetchSubagentHistory, historyEntryToSubagentInfo } from '@/shared/lib/omp/subagent/history/client';
|
|
6
6
|
import { subagentRowLabel } from '@/shared/lib/omp/subagent/label';
|
|
7
|
-
import { mergeSubagentRoster, parseSubagentLifecycle,
|
|
7
|
+
import { mergeSubagentRoster, parseSubagentLifecycle, parseSubagentRosterResponse, readSubagentProgressFrame } from '@/shared/lib/omp/subagent/parse';
|
|
8
8
|
import type { SubagentInfo, SubagentProgress } from '@/shared/types';
|
|
9
9
|
|
|
10
10
|
type SubagentListProps = {
|
|
@@ -18,14 +18,6 @@ const PROGRESS_STATUS: Record<NonNullable<SubagentProgress['status']>, SubagentI
|
|
|
18
18
|
pending: 'started', running: 'started', completed: 'completed', failed: 'failed', aborted: 'aborted',
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
/** Progress frames arrive AgentProgress-shaped; some wrappers nest the snapshot. */
|
|
22
|
-
function readProgress(payload: unknown): SubagentProgress | undefined {
|
|
23
|
-
const direct = parseSubagentProgress(payload);
|
|
24
|
-
if (!isRecord(payload) || !isRecord(payload.progress)) return direct;
|
|
25
|
-
const nested = parseSubagentProgress(payload.progress);
|
|
26
|
-
return nested ? { ...nested, id: nested.id ?? direct?.id, index: nested.index ?? direct?.index } : direct;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
21
|
/** Fold one progress frame into the roster entry it names (id, else index). */
|
|
30
22
|
function applyProgress(roster: SubagentInfo[], progress: SubagentProgress): SubagentInfo[] {
|
|
31
23
|
return roster.map((entry) => {
|
|
@@ -44,6 +36,8 @@ function applyProgress(roster: SubagentInfo[], progress: SubagentProgress): Suba
|
|
|
44
36
|
/**
|
|
45
37
|
* Subagent roster nested under a session row: renders clean, readable subagent
|
|
46
38
|
* items matching the minimalist sidebar layout while preserving interactive inspection.
|
|
39
|
+
* Shared by the desktop sidebar's `CategoryItem` and the mobile drawer's
|
|
40
|
+
* `MobileSessionCategory`, so both list the same roster.
|
|
47
41
|
*/
|
|
48
42
|
export function SubagentList({ sessionId, isActiveSession }: SubagentListProps) {
|
|
49
43
|
const [searchParams] = useSearchParams();
|
|
@@ -85,13 +79,17 @@ export function SubagentList({ sessionId, isActiveSession }: SubagentListProps)
|
|
|
85
79
|
}
|
|
86
80
|
};
|
|
87
81
|
const onProgress = (event: Event) => {
|
|
88
|
-
const progress =
|
|
82
|
+
const progress = readSubagentProgressFrame(detailOf(event)?.payload);
|
|
89
83
|
if (progress) setSubagents((prev) => applyProgress(prev, progress));
|
|
90
84
|
};
|
|
91
85
|
|
|
92
86
|
window.addEventListener('subagent_lifecycle', onLifecycle);
|
|
93
87
|
window.addEventListener('subagent_progress', onProgress);
|
|
94
88
|
|
|
89
|
+
// Snapshot the LIVE registry. This POST is the spawn path, so the server
|
|
90
|
+
// refuses it (409) for a session it does not manage — an observer read must
|
|
91
|
+
// never boot an omp child. The history pass above already covers a finished
|
|
92
|
+
// session from disk, so nothing is missing when this comes back empty.
|
|
95
93
|
fetch(`/api/agent/${encodeURIComponent(sid)}`, {
|
|
96
94
|
method: 'POST',
|
|
97
95
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -6,9 +6,9 @@ import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
|
6
6
|
import { useShowMore } from '@/client/hooks/ui/show-more';
|
|
7
7
|
import { useWorkspaceFolderActions } from '@/client/hooks/workspace/workspace-folder-actions';
|
|
8
8
|
import { SessionItem } from '@/client/components/layout/session-sidebar/SessionItem';
|
|
9
|
-
import { SubagentList } from '@/client/components/
|
|
9
|
+
import { SubagentList } from '@/client/components/common/subagent-list';
|
|
10
10
|
import { WorkspaceOptionsMenu } from '@/client/components/common/workspace-options-menu';
|
|
11
|
-
import {
|
|
11
|
+
import { useExpandedSessions } from '@/client/hooks/workspace/expanded-sessions';
|
|
12
12
|
import { getProjectIcon } from '@/shared/lib/workspace/project-icon';
|
|
13
13
|
import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
|
|
14
14
|
|
|
@@ -37,32 +37,14 @@ export function Category({
|
|
|
37
37
|
const [searchParams] = useSearchParams();
|
|
38
38
|
const urlSubagentId = searchParams.get('subagent');
|
|
39
39
|
const urlSessionId = searchParams.get('sessionId');
|
|
40
|
-
|
|
41
|
-
// Sidebar-level expanded session set
|
|
42
|
-
const [expandedSessionIds, setExpandedSessionIds] = useState<Set<string>>(() => new Set());
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
setExpandedSessionIds(saved);
|
|
48
|
-
}
|
|
49
|
-
}, []);
|
|
41
|
+
// Roster expansion is shared with the mobile drawer: one persisted set, so a
|
|
42
|
+
// row expanded on the phone is expanded here too.
|
|
43
|
+
const { expandedSessionIds, toggleSession } = useExpandedSessions();
|
|
50
44
|
|
|
51
45
|
useEffect(() => {
|
|
52
46
|
if (typeof folder.isExpanded === 'boolean') setIsOpen(folder.isExpanded);
|
|
53
47
|
}, [folder.isExpanded]);
|
|
54
|
-
|
|
55
|
-
// A deep-linked transcript (?sessionId=…&subagent=…) auto-expands its
|
|
56
|
-
// session row so the viewed roster entry is visible after a reload.
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
if (!urlSubagentId || !urlSessionId) return;
|
|
59
|
-
setExpandedSessionIds(prev => {
|
|
60
|
-
if (prev.has(urlSessionId)) return prev;
|
|
61
|
-
const next = new Set(prev);
|
|
62
|
-
next.add(urlSessionId);
|
|
63
|
-
return next;
|
|
64
|
-
});
|
|
65
|
-
}, [urlSubagentId, urlSessionId]);
|
|
66
48
|
|
|
67
49
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
68
50
|
const { refresh } = useSidebarData();
|
|
@@ -106,13 +88,7 @@ export function Category({
|
|
|
106
88
|
};
|
|
107
89
|
|
|
108
90
|
const handleToggleSessionExpand = (sessionId: string) => {
|
|
109
|
-
|
|
110
|
-
const next = new Set(prev);
|
|
111
|
-
if (next.has(sessionId)) next.delete(sessionId);
|
|
112
|
-
else next.add(sessionId);
|
|
113
|
-
saveExpandedSessionIds(next);
|
|
114
|
-
return next;
|
|
115
|
-
});
|
|
91
|
+
toggleSession(sessionId);
|
|
116
92
|
};
|
|
117
93
|
|
|
118
94
|
const onPin = () => {
|
|
@@ -18,6 +18,8 @@ interface SessionSidebarToolbarProps {
|
|
|
18
18
|
onNewWorkspace: () => void;
|
|
19
19
|
onScheduler: () => void;
|
|
20
20
|
onRefresh: () => void;
|
|
21
|
+
/** True while the user-initiated refresh is in flight — spins the icon. */
|
|
22
|
+
refreshing: boolean;
|
|
21
23
|
onClose?: () => void;
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -37,6 +39,7 @@ export function SessionSidebarToolbar({
|
|
|
37
39
|
onNewWorkspace,
|
|
38
40
|
onScheduler,
|
|
39
41
|
onRefresh,
|
|
42
|
+
refreshing,
|
|
40
43
|
onClose,
|
|
41
44
|
}: SessionSidebarToolbarProps) {
|
|
42
45
|
return (
|
|
@@ -45,16 +48,19 @@ export function SessionSidebarToolbar({
|
|
|
45
48
|
<div className="flex space-x-3 items-center">
|
|
46
49
|
<FolderPlus size={14} className="hover:text-ink cursor-pointer" onClick={onNewWorkspace} />
|
|
47
50
|
<Calendar size={14} className="hover:text-ink cursor-pointer" onClick={onScheduler} />
|
|
48
|
-
<
|
|
49
|
-
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
title="Refresh sessions"
|
|
50
54
|
onClick={onRefresh}
|
|
51
|
-
className="inline-flex cursor-pointer"
|
|
55
|
+
className="inline-flex cursor-pointer bg-transparent border-0 p-0 text-ink/60"
|
|
56
|
+
aria-label="Refresh sessions"
|
|
57
|
+
aria-busy={refreshing}
|
|
52
58
|
>
|
|
53
59
|
<RefreshCw
|
|
54
60
|
size={14}
|
|
55
|
-
className=
|
|
61
|
+
className={`hover:text-ink ${refreshing ? 'animate-spin' : ''}`}
|
|
56
62
|
/>
|
|
57
|
-
</
|
|
63
|
+
</button>
|
|
58
64
|
</div>
|
|
59
65
|
<div className="flex space-x-3 items-center relative">
|
|
60
66
|
<Search
|
|
@@ -17,7 +17,8 @@ export function SessionSidebar({ className = '', onClose, appSettings = {} }: {
|
|
|
17
17
|
const {
|
|
18
18
|
folders,
|
|
19
19
|
initializing,
|
|
20
|
-
|
|
20
|
+
refreshNow,
|
|
21
|
+
refreshing,
|
|
21
22
|
activeSessionId,
|
|
22
23
|
sessionParam,
|
|
23
24
|
searchQuery,
|
|
@@ -105,7 +106,8 @@ export function SessionSidebar({ className = '', onClose, appSettings = {} }: {
|
|
|
105
106
|
onSortChange={handleSortChange}
|
|
106
107
|
onNewWorkspace={() => setNewWorkspaceOpen(true)}
|
|
107
108
|
onScheduler={() => setSchedulerOpen(true)}
|
|
108
|
-
onRefresh={
|
|
109
|
+
onRefresh={refreshNow}
|
|
110
|
+
refreshing={refreshing}
|
|
109
111
|
onClose={onClose}
|
|
110
112
|
/>
|
|
111
113
|
|
|
@@ -74,6 +74,12 @@ export function MobileLayoutWrapper({ onDesktopToggle, appSettings = {} }: Mobil
|
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
// A roster row opens a subagent transcript in the chat timeline (the
|
|
78
|
+
// `omp:view-subagent` listener there owns the URL). On the phone the chat is
|
|
79
|
+
// a separate screen, so the tap must also bring it forward — otherwise the
|
|
80
|
+
// transcript opens behind the drawer and the tap reads as a no-op.
|
|
81
|
+
useChamberEvent('omp:view-subagent', () => setCurrentScreen('main'));
|
|
82
|
+
|
|
77
83
|
// Settings requests (e.g. the model dropdown asking for Settings → Providers
|
|
78
84
|
// when no provider is configured) arrive on the same global channel the
|
|
79
85
|
// desktop layout listens on. SettingsModal itself only reads the payload and
|
|
@@ -36,6 +36,8 @@ export function MobileSessionSidebar({
|
|
|
36
36
|
const {
|
|
37
37
|
folders,
|
|
38
38
|
initializing,
|
|
39
|
+
refreshNow,
|
|
40
|
+
refreshing,
|
|
39
41
|
searchQuery,
|
|
40
42
|
setSearchQuery,
|
|
41
43
|
showArchived,
|
|
@@ -124,6 +126,8 @@ export function MobileSessionSidebar({
|
|
|
124
126
|
onNewSession={handleNewSessionAndClose}
|
|
125
127
|
onNewWorkspace={() => setNewWorkspaceOpen(true)}
|
|
126
128
|
onScheduler={() => setSchedulerOpen(true)}
|
|
129
|
+
onRefresh={refreshNow}
|
|
130
|
+
refreshing={refreshing}
|
|
127
131
|
onToggleOptions={() => setOptionsOpen(!optionsOpen)}
|
|
128
132
|
onSortChange={handleSortChange}
|
|
129
133
|
onResetSort={() => handleSortChange('A-Z')}
|