ompchamber 3.1.1 → 3.3.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 +306 -298
- package/dist/client/static/js/index-bsxxt89j.css +1 -0
- package/dist/client/static/js/{index-qhzsybkn.js → index-qkt4z7x0.js} +658 -658
- package/package.json +4 -3
- package/public/sw.js +54 -5
- package/src/cli/lib/output.js +15 -8
- 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-right-sidebar/FullEditor.tsx +27 -8
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +67 -29
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.test.ts +84 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +59 -12
- package/src/client/components/mobile/mobile-session-sidebar/Toolbar.tsx +21 -1
- package/src/client/components/settings/Modal.tsx +45 -23
- 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/AppearanceSettings.tsx +3 -0
- 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/appearance-settings/EditorFontSection.tsx +78 -0
- 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 +19 -12
- 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/editor/index.tsx +15 -7
- package/src/client/components/workspace/git-panel/commit-modal/CommitDiffViewer.tsx +2 -1
- package/src/client/components/workspace/terminal-panel/KeyBar.tsx +194 -0
- package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +24 -0
- package/src/client/components/workspace/terminal-panel/index.tsx +98 -3
- package/src/client/components/workspace/terminal-panel/mount.ts +47 -0
- package/src/client/data/theme/terminal.ts +25 -8
- 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/font.ts +53 -0
- 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/ui/clipboard.ts +17 -0
- package/src/client/hooks/ui/keyboard-inset.test.ts +46 -0
- package/src/client/hooks/ui/keyboard-inset.ts +113 -0
- package/src/client/hooks/ui/text-overflow.test.ts +152 -0
- package/src/client/hooks/ui/text-overflow.ts +78 -0
- package/src/client/hooks/ui/touch-device.ts +46 -0
- package/src/client/hooks/workspace/expanded-sessions.ts +62 -0
- package/src/client/main.tsx +1 -0
- package/src/server/index.ts +4 -3
- package/src/server/lib/assets/conditional.server.test.ts +74 -0
- package/src/server/lib/assets/conditional.server.ts +54 -0
- package/src/server/lib/assets/dev-assets.server.test.ts +44 -0
- package/src/server/lib/assets/dev-assets.server.ts +121 -0
- package/src/server/lib/assets/fonts.server.ts +9 -4
- package/src/server/lib/bundler/build-errors.server.ts +136 -0
- package/src/server/lib/bundler/build-errors.test.ts +103 -0
- package/src/server/lib/bundler/dependencies.test.ts +134 -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/listener.ts +29 -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/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/plugins/shell.server.ts +52 -13
- package/src/server/plugins/ssr.ts +43 -7
- package/src/server/plugins/static.ts +51 -18
- 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/editor/typography.test.ts +93 -0
- package/src/shared/lib/code/editor/typography.ts +86 -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/fonts/NOTICE.md +44 -0
- package/src/shared/lib/fonts/nerd-symbols.css +55 -0
- package/src/shared/lib/fonts/nerd-symbols.test.ts +134 -0
- package/src/shared/lib/fonts/symbols-nerd-font-mono.woff2 +0 -0
- 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/lib/settings/diff.test.ts +1 -1
- package/src/shared/lib/workspace/terminal/keys.test.ts +189 -0
- package/src/shared/lib/workspace/terminal/keys.ts +225 -0
- package/src/shared/types/omp/subagent.ts +6 -0
- package/src/shared/types/settings/state.ts +7 -16
- package/dist/client/static/js/index-5yfre1kw.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ompchamber",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.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>",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"@fontsource/fira-code": "^5.3.0",
|
|
51
51
|
"@shikijs/langs": "^4.4.3",
|
|
52
52
|
"@shikijs/themes": "^4.4.3",
|
|
53
|
+
"@tailwindcss/postcss": "^4.1.14",
|
|
53
54
|
"@xterm/addon-fit": "^0.11.0",
|
|
54
55
|
"@xterm/addon-webgl": "^0.19.0",
|
|
55
56
|
"@xterm/xterm": "^6.0.0",
|
|
@@ -60,10 +61,12 @@
|
|
|
60
61
|
"marked": "^18.0.11",
|
|
61
62
|
"marked-linkify-it": "^4.0.2",
|
|
62
63
|
"mermaid": "^12.0.0",
|
|
64
|
+
"postcss": "^8.5.16",
|
|
63
65
|
"preact": "^10.29.8",
|
|
64
66
|
"remend": "^1.3.1",
|
|
65
67
|
"ripgrep": "^0.3.1",
|
|
66
68
|
"shiki": "^4.4.3",
|
|
69
|
+
"tailwindcss": "^4.1.14",
|
|
67
70
|
"yaml": "^2.9.1"
|
|
68
71
|
},
|
|
69
72
|
"devDependencies": {
|
|
@@ -73,13 +76,11 @@
|
|
|
73
76
|
"@semantic-release/github": "^12.0.10",
|
|
74
77
|
"@semantic-release/npm": "^13.2.0",
|
|
75
78
|
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
76
|
-
"@tailwindcss/postcss": "^4.1.14",
|
|
77
79
|
"@types/bun": "^1.4.2",
|
|
78
80
|
"@types/node": "^22.14.0",
|
|
79
81
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
80
82
|
"happy-dom": "^20.14.5",
|
|
81
83
|
"semantic-release": "^25.0.9",
|
|
82
|
-
"tailwindcss": "^4.1.14",
|
|
83
84
|
"typescript": "~5.8.2"
|
|
84
85
|
}
|
|
85
86
|
}
|
package/public/sw.js
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* OMPChamber service worker.
|
|
3
|
+
*
|
|
4
|
+
* Two jobs, and only the first one is about installability.
|
|
5
|
+
*
|
|
6
|
+
* 1. Chromium requires a fetch handler for PWA installability.
|
|
7
|
+
*
|
|
8
|
+
* 2. The dev client bundle is cached here rather than by the HTTP cache,
|
|
9
|
+
* because the HTTP cache CANNOT hold it. Chrome declines to store a response
|
|
10
|
+
* whose decoded body reaches ~5 MB — measured with `Cache-Control: no-cache`
|
|
11
|
+
* and a working `ETag`: 1 MB and 4 MB answered `304` on the next load, while
|
|
12
|
+
* 5, 6, 7, 8, 16 and 20 MB were re-fetched in full every time, never sending
|
|
13
|
+
* `If-None-Match`. Bun's development bundle is ~19.7 MB (unminified, by
|
|
14
|
+
* design), so it was re-downloaded on every single page load — the whole of
|
|
15
|
+
* the dev-mode bandwidth complaint. Cache Storage has no such per-entry
|
|
16
|
+
* limit, so this is the one layer that can hold it.
|
|
17
|
+
*
|
|
18
|
+
* Only `/_dev-assets/client/*` is cached: that is the bundle, whose filename
|
|
19
|
+
* carries a build-generation hash that changes on every rebuild, so a URL is a
|
|
20
|
+
* reliable key for its bytes. `/_dev-assets/asset/*` is deliberately NOT
|
|
21
|
+
* cached — a CSS asset keeps its name while its content changes (appending a
|
|
22
|
+
* rule to `tailwind.css` changes the bytes served at the same URL), so
|
|
23
|
+
* cache-first there would serve stale styles; those go to the network and are
|
|
24
|
+
* revalidated by the server's own `ETag`, which answers `304`.
|
|
25
|
+
*
|
|
26
|
+
* One bundle generation is kept at a time: storing a new one drops the rest, so
|
|
27
|
+
* the cache cannot grow by one bundle per rebuild. Nothing here runs in
|
|
28
|
+
* production — the URLs it matches do not exist there.
|
|
29
|
+
*/
|
|
2
30
|
|
|
3
|
-
|
|
31
|
+
const BUNDLE_CACHE = 'ompchamber-dev-bundle';
|
|
32
|
+
const BUNDLE_PREFIX = '/_dev-assets/client/';
|
|
33
|
+
|
|
34
|
+
self.addEventListener('install', () => {
|
|
4
35
|
self.skipWaiting();
|
|
5
36
|
});
|
|
6
37
|
|
|
@@ -9,7 +40,25 @@ self.addEventListener('activate', (event) => {
|
|
|
9
40
|
});
|
|
10
41
|
|
|
11
42
|
self.addEventListener('fetch', (event) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
43
|
+
const url = new URL(event.request.url);
|
|
44
|
+
if (event.request.method !== 'GET') return;
|
|
45
|
+
if (url.origin !== self.location.origin) return;
|
|
46
|
+
if (!url.pathname.startsWith(BUNDLE_PREFIX)) return;
|
|
47
|
+
|
|
48
|
+
event.respondWith(
|
|
49
|
+
(async () => {
|
|
50
|
+
const cache = await caches.open(BUNDLE_CACHE);
|
|
51
|
+
const hit = await cache.match(url.pathname);
|
|
52
|
+
if (hit) return hit;
|
|
53
|
+
|
|
54
|
+
const response = await fetch(event.request);
|
|
55
|
+
if (response.ok) {
|
|
56
|
+
await cache.put(url.pathname, response.clone());
|
|
57
|
+
// The previous generation is dead the moment a new one is stored.
|
|
58
|
+
const stale = await cache.keys();
|
|
59
|
+
await Promise.all(stale.filter((key) => new URL(key.url).pathname !== url.pathname).map((key) => cache.delete(key)));
|
|
60
|
+
}
|
|
61
|
+
return response;
|
|
62
|
+
})(),
|
|
63
|
+
);
|
|
15
64
|
});
|
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();
|
|
@@ -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')}
|
|
@@ -6,6 +6,13 @@ import { ImageViewer } from '@/client/components/common/image-viewer';
|
|
|
6
6
|
import { EditorHeader } from '@/client/components/mobile/mobile-right-sidebar/Header';
|
|
7
7
|
import { useScrollbarFade, scrollbarFadeClass } from '@/client/hooks/ui/scrollbar-fade';
|
|
8
8
|
import { useFileEditor } from '@/client/hooks/editor/use-file-editor';
|
|
9
|
+
import {
|
|
10
|
+
EDITOR_DEFAULT_FONT_SIZE,
|
|
11
|
+
EDITOR_LINE_HEIGHT,
|
|
12
|
+
EDITOR_MAX_FONT_SIZE,
|
|
13
|
+
EDITOR_MIN_FONT_SIZE,
|
|
14
|
+
} from '@/shared/lib/code/editor/typography';
|
|
15
|
+
import { useEditorFontStack } from '@/client/hooks/editor/font';
|
|
9
16
|
|
|
10
17
|
interface MobileFullEditorProps {
|
|
11
18
|
file: {
|
|
@@ -22,9 +29,10 @@ interface MobileFullEditorProps {
|
|
|
22
29
|
|
|
23
30
|
export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEditorProps) {
|
|
24
31
|
const [isPreview, setIsPreview] = useState(file.name.endsWith('.md'));
|
|
25
|
-
const [fontSize, setFontSize] = useState(
|
|
32
|
+
const [fontSize, setFontSize] = useState(EDITOR_DEFAULT_FONT_SIZE);
|
|
26
33
|
const [wordWrap, setWordWrap] = useState(true);
|
|
27
34
|
const { isScrolling, handleScroll } = useScrollbarFade();
|
|
35
|
+
const editorFontFamily = useEditorFontStack();
|
|
28
36
|
|
|
29
37
|
const isMd = file.name.endsWith('.md');
|
|
30
38
|
|
|
@@ -52,8 +60,8 @@ export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEdito
|
|
|
52
60
|
onClose={onClose}
|
|
53
61
|
onTogglePreview={() => setIsPreview(!isPreview)}
|
|
54
62
|
onToggleWrap={() => setWordWrap(!wordWrap)}
|
|
55
|
-
onZoomOut={() => setFontSize(prev => Math.max(
|
|
56
|
-
onZoomIn={() => setFontSize(prev => Math.min(
|
|
63
|
+
onZoomOut={() => setFontSize(prev => Math.max(EDITOR_MIN_FONT_SIZE, prev - 1))}
|
|
64
|
+
onZoomIn={() => setFontSize(prev => Math.min(EDITOR_MAX_FONT_SIZE, prev + 1))}
|
|
57
65
|
onCopy={editor.copy}
|
|
58
66
|
onDownload={editor.download}
|
|
59
67
|
/>
|
|
@@ -80,7 +88,7 @@ export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEdito
|
|
|
80
88
|
/* Markdown Preview Mode — styled by `.prose-content` (the chat
|
|
81
89
|
timeline's system), see the desktop editor for why there is no
|
|
82
90
|
`prose` wrapper. */
|
|
83
|
-
<div className="p-4 bg-paper text-ink min-h-full font-sans">
|
|
91
|
+
<div className="p-4 bg-paper text-ink min-h-full font-sans" style={{ fontSize: `${fontSize}px` }}>
|
|
84
92
|
<MarkdownRenderer
|
|
85
93
|
content={content}
|
|
86
94
|
document
|
|
@@ -95,13 +103,24 @@ export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEdito
|
|
|
95
103
|
language={lang}
|
|
96
104
|
wordWrap={wordWrap}
|
|
97
105
|
rootClassName="flex min-h-full bg-paper"
|
|
98
|
-
|
|
106
|
+
// The gutter auto-sizes to its widest number rather than taking a
|
|
107
|
+
// fixed width: the numbers scale with the zoom control, and a
|
|
108
|
+
// pinned `w-10` clipped a 3-digit line number at 18px.
|
|
109
|
+
gutterClassName="flex flex-col text-right pr-2 select-none text-ink/30 bg-canvas border-r border-ink/10 font-mono flex-shrink-0"
|
|
110
|
+
gutterLineClassName="min-w-[1.5rem]"
|
|
111
|
+
gutterStyle={{
|
|
112
|
+
fontSize,
|
|
113
|
+
paddingTop: 12,
|
|
114
|
+
paddingBottom: 12,
|
|
115
|
+
lineHeight: EDITOR_LINE_HEIGHT,
|
|
116
|
+
fontFamily: editorFontFamily,
|
|
117
|
+
}}
|
|
99
118
|
editorWrapperClassName="flex-1 p-3 overflow-x-auto min-w-0 bg-paper text-ink code-surface"
|
|
100
119
|
editorClassName="focus:outline-none"
|
|
101
120
|
editorStyle={{
|
|
102
|
-
fontFamily:
|
|
103
|
-
fontSize
|
|
104
|
-
lineHeight:
|
|
121
|
+
fontFamily: editorFontFamily,
|
|
122
|
+
fontSize,
|
|
123
|
+
lineHeight: EDITOR_LINE_HEIGHT,
|
|
105
124
|
backgroundColor: 'transparent',
|
|
106
125
|
minHeight: '100%',
|
|
107
126
|
color: 'var(--theme-ink)',
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { ChevronDown, ChevronRight, GitBranch, MessageSquare, MoreHorizontal, Plus } from 'lucide-preact';
|
|
3
|
+
import { useSearchParams } from '@/client/lib/router/search-params';
|
|
3
4
|
import { MobileSessionRow } from '@/client/components/mobile/mobile-session-sidebar/SessionRow';
|
|
5
|
+
import { SubagentList } from '@/client/components/common/subagent-list';
|
|
4
6
|
import { WorkspaceOptionsMenu } from '@/client/components/common/workspace-options-menu';
|
|
5
7
|
import { useShowMore } from '@/client/hooks/ui/show-more';
|
|
8
|
+
import { useExpandedSessions } from '@/client/hooks/workspace/expanded-sessions';
|
|
6
9
|
import { useWorkspaceFolderActions } from '@/client/hooks/workspace/workspace-folder-actions';
|
|
7
10
|
import { getProjectIcon } from '@/shared/lib/workspace/project-icon';
|
|
8
11
|
import { relativeTimeAgo } from '@/shared/lib/workspace/relative-time';
|
|
@@ -33,6 +36,12 @@ export function MobileSessionCategory({
|
|
|
33
36
|
}: MobileSessionCategoryProps) {
|
|
34
37
|
const { visibleCount, showMore } = useShowMore();
|
|
35
38
|
const { refresh } = useSidebarData();
|
|
39
|
+
const { expandedSessionIds, toggleSession } = useExpandedSessions();
|
|
40
|
+
// The viewed transcript, so its session row dims while a roster entry is
|
|
41
|
+
// open (desktop parity — same params, same rule).
|
|
42
|
+
const [searchParams] = useSearchParams();
|
|
43
|
+
const urlSubagentId = searchParams.get('subagent');
|
|
44
|
+
const urlSessionId = searchParams.get('sessionId');
|
|
36
45
|
const [showMenu, setShowMenu] = useState(false);
|
|
37
46
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
38
47
|
const {
|
|
@@ -72,25 +81,32 @@ export function MobileSessionCategory({
|
|
|
72
81
|
<div className="mb-4">
|
|
73
82
|
<div
|
|
74
83
|
onClick={onToggleExpand}
|
|
75
|
-
className="flex items-center justify-between
|
|
84
|
+
className="flex items-center justify-between pl-4 pr-3 py-2 cursor-pointer hover:bg-ink/5 rounded-lg select-none transition-colors"
|
|
76
85
|
>
|
|
77
86
|
<div className="flex items-center space-x-2 min-w-0">
|
|
78
|
-
{folder
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
{/* The folder glyph occupies the same 16px left slot the session rows
|
|
88
|
+
give their roster toggle, so the two columns line up down the
|
|
89
|
+
drawer — the icon, the chevron and every row's status glyph share
|
|
90
|
+
one x. Desktop gets this from one shared slot; the drawer's
|
|
91
|
+
padding is what differs, hence the matching `pl-4`. */}
|
|
92
|
+
<span className="w-4 h-4 flex items-center justify-center flex-shrink-0">
|
|
93
|
+
{folder.customIconUrl ? (
|
|
94
|
+
<img
|
|
95
|
+
src={folder.customIconUrl}
|
|
96
|
+
alt=""
|
|
97
|
+
className="w-[15px] h-[15px] rounded-xs object-contain"
|
|
98
|
+
referrerPolicy="no-referrer"
|
|
99
|
+
/>
|
|
100
|
+
) : folder.iconType === 'chat' ? (
|
|
101
|
+
<MessageSquare size={15} className="text-ink/80" />
|
|
102
|
+
) : (
|
|
103
|
+
<ProjectIcon
|
|
104
|
+
size={15}
|
|
105
|
+
className="text-ink/80"
|
|
106
|
+
style={{ color: folder.accentColor || undefined }}
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
</span>
|
|
94
110
|
|
|
95
111
|
<span className="text-sm font-semibold text-ink truncate">
|
|
96
112
|
{folder.name}
|
|
@@ -153,18 +169,40 @@ export function MobileSessionCategory({
|
|
|
153
169
|
|
|
154
170
|
{isExpanded && (
|
|
155
171
|
<div className="mt-0.5 space-y-0.5 pl-2">
|
|
156
|
-
{visibleSessions.map((session) =>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
{visibleSessions.map((session) => {
|
|
173
|
+
const sessionKey = String(session.id);
|
|
174
|
+
const isSessionActive = String(activeSessionId) === sessionKey;
|
|
175
|
+
// While one of its subagents is being viewed, the parent session
|
|
176
|
+
// row dims so the highlighted roster entry reads as the active one
|
|
177
|
+
// (desktop parity).
|
|
178
|
+
const isViewingSubagent = isSessionActive && urlSessionId === sessionKey && Boolean(urlSubagentId);
|
|
179
|
+
// Same gate as desktop: the omp-side loader flag is authoritative,
|
|
180
|
+
// so a session whose roster exists on disk can be opened without a
|
|
181
|
+
// live process.
|
|
182
|
+
const hasSubagents = Boolean(session.hasSubagents);
|
|
183
|
+
const isRosterOpen = hasSubagents && expandedSessionIds.has(sessionKey);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div key={session.id} className="space-y-0.5">
|
|
187
|
+
<MobileSessionRow
|
|
188
|
+
session={session}
|
|
189
|
+
isActive={isSessionActive && !isViewingSubagent}
|
|
190
|
+
status={sessionStatus[sessionKey]}
|
|
191
|
+
awaitingInput={Boolean(session.awaitingInput)}
|
|
192
|
+
timeAgo={relativeTimeAgo(session.updated_at ?? session.created_at)}
|
|
193
|
+
hasSubagents={hasSubagents}
|
|
194
|
+
isExpanded={isRosterOpen}
|
|
195
|
+
onToggleExpand={() => toggleSession(sessionKey)}
|
|
196
|
+
onSelect={() => onSelectSession(session.id)}
|
|
197
|
+
onArchive={() => handleArchive(session)}
|
|
198
|
+
onRename={sessionKey.startsWith('new-') ? undefined : (name) => void handleRename(session, name)}
|
|
199
|
+
/>
|
|
200
|
+
{isRosterOpen && (
|
|
201
|
+
<SubagentList sessionId={session.id} isActiveSession={isSessionActive} />
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
})}
|
|
168
206
|
|
|
169
207
|
{filteredSessions.length > visibleCount && (
|
|
170
208
|
<button
|