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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Language detection for the highlighter: which Shiki grammar a path maps to,
|
|
8
|
+
* and whether a block of text looks like code at all.
|
|
9
|
+
*
|
|
10
|
+
* Both are pure string predicates over the input, with no highlighter state, so
|
|
11
|
+
* they live beside the tokenizer rather than inside it — the tokenizer needs a
|
|
12
|
+
* booted Shiki instance, these do not, and a caller that only wants to pick a
|
|
13
|
+
* language (a tool card deciding whether to render a code surface) does not
|
|
14
|
+
* have to pull one in.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Map file extension to Shiki language id */
|
|
18
|
+
export function getLanguageFromPath(filePath?: string): string {
|
|
19
|
+
if (!filePath) return 'javascript';
|
|
20
|
+
const clean = filePath.split('?')[0].split('#')[0].replace(/:\d+(?:-\d+)?$/, '');
|
|
21
|
+
const ext = clean.split('.').pop()?.toLowerCase();
|
|
22
|
+
switch (ext) {
|
|
23
|
+
case 'ts': return 'typescript';
|
|
24
|
+
case 'tsx': return 'tsx';
|
|
25
|
+
case 'js':
|
|
26
|
+
case 'mjs':
|
|
27
|
+
case 'cjs': return 'javascript';
|
|
28
|
+
case 'jsx': return 'jsx';
|
|
29
|
+
case 'json': return 'json';
|
|
30
|
+
case 'md':
|
|
31
|
+
case 'markdown': return 'markdown';
|
|
32
|
+
case 'css': return 'css';
|
|
33
|
+
case 'sh':
|
|
34
|
+
case 'bash':
|
|
35
|
+
case 'zsh': return 'bash';
|
|
36
|
+
case 'diff':
|
|
37
|
+
case 'patch': return 'diff';
|
|
38
|
+
case 'yaml':
|
|
39
|
+
case 'yml': return 'yaml';
|
|
40
|
+
case 'html': return 'html';
|
|
41
|
+
case 'svg':
|
|
42
|
+
case 'xml': return 'xml';
|
|
43
|
+
case 'py': return 'python';
|
|
44
|
+
case 'go': return 'go';
|
|
45
|
+
case 'rs': return 'rust';
|
|
46
|
+
case 'sql': return 'sql';
|
|
47
|
+
case 'toml': return 'toml';
|
|
48
|
+
case 'java': return 'java';
|
|
49
|
+
case 'c':
|
|
50
|
+
case 'h': return 'c';
|
|
51
|
+
case 'cpp':
|
|
52
|
+
case 'cc':
|
|
53
|
+
case 'hpp': return 'cpp';
|
|
54
|
+
case 'cs': return 'csharp';
|
|
55
|
+
case 'rb': return 'ruby';
|
|
56
|
+
case 'php': return 'php';
|
|
57
|
+
case 'kt':
|
|
58
|
+
case 'kts': return 'kotlin';
|
|
59
|
+
case 'swift': return 'swift';
|
|
60
|
+
case 'scala': return 'scala';
|
|
61
|
+
case 'lua': return 'lua';
|
|
62
|
+
case 'pl': return 'perl';
|
|
63
|
+
case 'dockerfile': return 'dockerfile';
|
|
64
|
+
case 'ini':
|
|
65
|
+
case 'cfg':
|
|
66
|
+
case 'conf': return 'ini';
|
|
67
|
+
case 'properties': return 'properties';
|
|
68
|
+
case 'env': return 'bash';
|
|
69
|
+
case 'zig': return 'zig';
|
|
70
|
+
case 'ex':
|
|
71
|
+
case 'exs': return 'elixir';
|
|
72
|
+
case 'dart': return 'dart';
|
|
73
|
+
case 'ml':
|
|
74
|
+
case 'mli': return 'ocaml';
|
|
75
|
+
case 'hs': return 'haskell';
|
|
76
|
+
case 'nix': return 'nix';
|
|
77
|
+
case 'tf':
|
|
78
|
+
case 'tfvars':
|
|
79
|
+
case 'hcl': return 'hcl';
|
|
80
|
+
case 'graphql':
|
|
81
|
+
case 'gql': return 'graphql';
|
|
82
|
+
case 'sol': return 'solidity';
|
|
83
|
+
case 'r': return 'r';
|
|
84
|
+
case 'jl': return 'julia';
|
|
85
|
+
case 'erl': return 'erlang';
|
|
86
|
+
case 'clj':
|
|
87
|
+
case 'cljs':
|
|
88
|
+
case 'cljc':
|
|
89
|
+
case 'edn': return 'clojure';
|
|
90
|
+
case 'scm':
|
|
91
|
+
case 'ss': return 'scheme';
|
|
92
|
+
case 'rkt': return 'racket';
|
|
93
|
+
case 'proto': return 'protobuf';
|
|
94
|
+
case 'makefile':
|
|
95
|
+
case 'mk': return 'makefile';
|
|
96
|
+
case 'ps1':
|
|
97
|
+
case 'psm1': return 'powershell';
|
|
98
|
+
case 'json5': return 'json5';
|
|
99
|
+
case 'less': return 'less';
|
|
100
|
+
case 'scss': return 'scss';
|
|
101
|
+
case 'sass': return 'sass';
|
|
102
|
+
case 'styl': return 'stylus';
|
|
103
|
+
case 'vue': return 'vue';
|
|
104
|
+
case 'svelte': return 'svelte';
|
|
105
|
+
case 'astro': return 'astro';
|
|
106
|
+
// `.m` is genuinely ambiguous (Objective-C vs MATLAB); map to Objective-C,
|
|
107
|
+
// the common editor default.
|
|
108
|
+
case 'mm':
|
|
109
|
+
case 'm': return 'objective-c';
|
|
110
|
+
case 'groovy':
|
|
111
|
+
case 'gvy':
|
|
112
|
+
case 'gradle': return 'groovy';
|
|
113
|
+
default: return 'javascript';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The patterns `isCodeLike` probes, at module scope.
|
|
119
|
+
*
|
|
120
|
+
* A regex *literal* inside the function body constructs a fresh `RegExp` on
|
|
121
|
+
* every call, and this runs per render from the notice, task-result and Bash
|
|
122
|
+
* cards. Compiled once here instead.
|
|
123
|
+
*/
|
|
124
|
+
const CODE_PATTERNS = [
|
|
125
|
+
/^(?:import|export)\s+.+from\s+['"][^'"]+['"]/m,
|
|
126
|
+
/^(?:const|let|var|function|class|interface|type|enum)\s+[A-Za-z0-9_$]+/m,
|
|
127
|
+
/(?:=>|\bconsole\.(?:log|error|warn)|return\s+|async\s+function)/,
|
|
128
|
+
/^\s*\d+:\s*(?:import|const|let|function|\/\/|\/\*|<|[A-Za-z])/m,
|
|
129
|
+
/^\s*\[[A-Za-z0-9_./-]+\.[a-zA-Z0-9]+\]\s*\n\d+:/m,
|
|
130
|
+
] as const;
|
|
131
|
+
|
|
132
|
+
/** Check if a string looks like code or structured programming text */
|
|
133
|
+
export function isCodeLike(text: string): boolean {
|
|
134
|
+
if (!text || text.length < 5) return false;
|
|
135
|
+
const trimmed = text.trim();
|
|
136
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
137
|
+
try {
|
|
138
|
+
JSON.parse(trimmed);
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
// not valid JSON
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return CODE_PATTERNS.some((pattern) => pattern.test(trimmed));
|
|
146
|
+
}
|
|
@@ -7,7 +7,8 @@ import { beforeAll, describe, expect, test } from 'bun:test';
|
|
|
7
7
|
|
|
8
8
|
import { bootSyntax, onLanguageReady, requestLanguage } from '@/shared/lib/code/highlighter';
|
|
9
9
|
import { findMatches } from '@/shared/lib/code/editor/find';
|
|
10
|
-
import { getLanguageFromPath
|
|
10
|
+
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
11
|
+
import { highlightCode, highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
11
12
|
import { highlightCodeWindow } from '@/shared/lib/code/windowed-highlight';
|
|
12
13
|
|
|
13
14
|
/** Shiki emits `<span class="shiki">…` with dual-theme CSS variables per token. */
|
|
@@ -7,128 +7,6 @@ import { SHIKI_THEMES } from '@/shared/lib/code/shiki-themes';
|
|
|
7
7
|
|
|
8
8
|
export type { HighlightLinesOptions };
|
|
9
9
|
|
|
10
|
-
/** Map file extension to Shiki language id */
|
|
11
|
-
export function getLanguageFromPath(filePath?: string): string {
|
|
12
|
-
if (!filePath) return 'javascript';
|
|
13
|
-
const clean = filePath.split('?')[0].split('#')[0].replace(/:\d+(?:-\d+)?$/, '');
|
|
14
|
-
const ext = clean.split('.').pop()?.toLowerCase();
|
|
15
|
-
switch (ext) {
|
|
16
|
-
case 'ts': return 'typescript';
|
|
17
|
-
case 'tsx': return 'tsx';
|
|
18
|
-
case 'js':
|
|
19
|
-
case 'mjs':
|
|
20
|
-
case 'cjs': return 'javascript';
|
|
21
|
-
case 'jsx': return 'jsx';
|
|
22
|
-
case 'json': return 'json';
|
|
23
|
-
case 'md':
|
|
24
|
-
case 'markdown': return 'markdown';
|
|
25
|
-
case 'css': return 'css';
|
|
26
|
-
case 'sh':
|
|
27
|
-
case 'bash':
|
|
28
|
-
case 'zsh': return 'bash';
|
|
29
|
-
case 'diff':
|
|
30
|
-
case 'patch': return 'diff';
|
|
31
|
-
case 'yaml':
|
|
32
|
-
case 'yml': return 'yaml';
|
|
33
|
-
case 'html': return 'html';
|
|
34
|
-
case 'svg':
|
|
35
|
-
case 'xml': return 'xml';
|
|
36
|
-
case 'py': return 'python';
|
|
37
|
-
case 'go': return 'go';
|
|
38
|
-
case 'rs': return 'rust';
|
|
39
|
-
case 'sql': return 'sql';
|
|
40
|
-
case 'toml': return 'toml';
|
|
41
|
-
case 'java': return 'java';
|
|
42
|
-
case 'c':
|
|
43
|
-
case 'h': return 'c';
|
|
44
|
-
case 'cpp':
|
|
45
|
-
case 'cc':
|
|
46
|
-
case 'hpp': return 'cpp';
|
|
47
|
-
case 'cs': return 'csharp';
|
|
48
|
-
case 'rb': return 'ruby';
|
|
49
|
-
case 'php': return 'php';
|
|
50
|
-
case 'kt':
|
|
51
|
-
case 'kts': return 'kotlin';
|
|
52
|
-
case 'swift': return 'swift';
|
|
53
|
-
case 'scala': return 'scala';
|
|
54
|
-
case 'lua': return 'lua';
|
|
55
|
-
case 'pl': return 'perl';
|
|
56
|
-
case 'dockerfile': return 'dockerfile';
|
|
57
|
-
case 'ini':
|
|
58
|
-
case 'cfg':
|
|
59
|
-
case 'conf': return 'ini';
|
|
60
|
-
case 'properties': return 'properties';
|
|
61
|
-
case 'env': return 'bash';
|
|
62
|
-
case 'zig': return 'zig';
|
|
63
|
-
case 'ex':
|
|
64
|
-
case 'exs': return 'elixir';
|
|
65
|
-
case 'dart': return 'dart';
|
|
66
|
-
case 'ml':
|
|
67
|
-
case 'mli': return 'ocaml';
|
|
68
|
-
case 'hs': return 'haskell';
|
|
69
|
-
case 'nix': return 'nix';
|
|
70
|
-
case 'tf':
|
|
71
|
-
case 'tfvars':
|
|
72
|
-
case 'hcl': return 'hcl';
|
|
73
|
-
case 'graphql':
|
|
74
|
-
case 'gql': return 'graphql';
|
|
75
|
-
case 'sol': return 'solidity';
|
|
76
|
-
case 'r': return 'r';
|
|
77
|
-
case 'jl': return 'julia';
|
|
78
|
-
case 'erl': return 'erlang';
|
|
79
|
-
case 'clj':
|
|
80
|
-
case 'cljs':
|
|
81
|
-
case 'cljc':
|
|
82
|
-
case 'edn': return 'clojure';
|
|
83
|
-
case 'scm':
|
|
84
|
-
case 'ss': return 'scheme';
|
|
85
|
-
case 'rkt': return 'racket';
|
|
86
|
-
case 'proto': return 'protobuf';
|
|
87
|
-
case 'makefile':
|
|
88
|
-
case 'mk': return 'makefile';
|
|
89
|
-
case 'ps1':
|
|
90
|
-
case 'psm1': return 'powershell';
|
|
91
|
-
case 'json5': return 'json5';
|
|
92
|
-
case 'less': return 'less';
|
|
93
|
-
case 'scss': return 'scss';
|
|
94
|
-
case 'sass': return 'sass';
|
|
95
|
-
case 'styl': return 'stylus';
|
|
96
|
-
case 'vue': return 'vue';
|
|
97
|
-
case 'svelte': return 'svelte';
|
|
98
|
-
case 'astro': return 'astro';
|
|
99
|
-
// `.m` is genuinely ambiguous (Objective-C vs MATLAB); map to Objective-C,
|
|
100
|
-
// the common editor default.
|
|
101
|
-
case 'mm':
|
|
102
|
-
case 'm': return 'objective-c';
|
|
103
|
-
case 'groovy':
|
|
104
|
-
case 'gvy':
|
|
105
|
-
case 'gradle': return 'groovy';
|
|
106
|
-
default: return 'javascript';
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Check if a string looks like code or structured programming text */
|
|
111
|
-
export function isCodeLike(text: string): boolean {
|
|
112
|
-
if (!text || text.length < 5) return false;
|
|
113
|
-
const trimmed = text.trim();
|
|
114
|
-
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
115
|
-
try {
|
|
116
|
-
JSON.parse(trimmed);
|
|
117
|
-
return true;
|
|
118
|
-
} catch {
|
|
119
|
-
// not valid JSON
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const codePatterns = [
|
|
124
|
-
/^(?:import|export)\s+.+from\s+['"][^'"]+['"]/m,
|
|
125
|
-
/^(?:const|let|var|function|class|interface|type|enum)\s+[A-Za-z0-9_$]+/m,
|
|
126
|
-
/(?:=>|\bconsole\.(?:log|error|warn)|return\s+|async\s+function)/,
|
|
127
|
-
/^\s*\d+:\s*(?:import|const|let|function|\/\/|\/\*|<|[A-Za-z])/m,
|
|
128
|
-
/^\s*\[[A-Za-z0-9_./-]+\.[a-zA-Z0-9]+\]\s*\n\d+:/m,
|
|
129
|
-
];
|
|
130
|
-
return codePatterns.some((pattern) => pattern.test(trimmed));
|
|
131
|
-
}
|
|
132
10
|
|
|
133
11
|
/**
|
|
134
12
|
* Pick a language that can be tokenized *synchronously* right now. A grammar
|
|
@@ -61,7 +61,7 @@ describe('parseUnifiedDiff', () => {
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
test('folds a whitespace-only rewrite into context when ignoring whitespace', () => {
|
|
64
|
-
const { lines, splitRows, additions, deletions } = parseUnifiedDiff(WHITESPACE_ONLY, true);
|
|
64
|
+
const { lines, splitRows, additions, deletions } = parseUnifiedDiff(WHITESPACE_ONLY, { ignoreWhitespace: true });
|
|
65
65
|
|
|
66
66
|
// The counts the toolbar renders must match what is on screen.
|
|
67
67
|
expect(additions).toBe(0);
|
|
@@ -81,7 +81,7 @@ describe('parseUnifiedDiff', () => {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
test('keeps a real change when ignoring whitespace', () => {
|
|
84
|
-
const { lines, additions, deletions } = parseUnifiedDiff(REAL_CHANGE, true);
|
|
84
|
+
const { lines, additions, deletions } = parseUnifiedDiff(REAL_CHANGE, { ignoreWhitespace: true });
|
|
85
85
|
|
|
86
86
|
expect(additions).toBe(1);
|
|
87
87
|
expect(deletions).toBe(1);
|
|
@@ -90,7 +90,7 @@ describe('parseUnifiedDiff', () => {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
test('folds only the whitespace pair of a mixed hunk', () => {
|
|
93
|
-
const { lines, additions, deletions } = parseUnifiedDiff(MIXED, true);
|
|
93
|
+
const { lines, additions, deletions } = parseUnifiedDiff(MIXED, { ignoreWhitespace: true });
|
|
94
94
|
|
|
95
95
|
// Line 1 differs by indentation only; line 2 is a genuine edit.
|
|
96
96
|
expect(additions).toBe(1);
|
|
@@ -110,7 +110,7 @@ describe('parseUnifiedDiff', () => {
|
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
test('returns empty results for an empty diff', () => {
|
|
113
|
-
expect(parseUnifiedDiff('', true)).toEqual({ lines: [], splitRows: [], additions: 0, deletions: 0 });
|
|
114
|
-
expect(parseUnifiedDiff(' \n', false)).toEqual({ lines: [], splitRows: [], additions: 0, deletions: 0 });
|
|
113
|
+
expect(parseUnifiedDiff('', { ignoreWhitespace: true })).toEqual({ lines: [], splitRows: [], additions: 0, deletions: 0 });
|
|
114
|
+
expect(parseUnifiedDiff(' \n', { ignoreWhitespace: false })).toEqual({ lines: [], splitRows: [], additions: 0, deletions: 0 });
|
|
115
115
|
});
|
|
116
116
|
});
|
|
@@ -31,12 +31,29 @@ interface PendingChange {
|
|
|
31
31
|
* which is what this used to do — left the row painted red/green and counted in
|
|
32
32
|
* `+n/-n`, so the toggle changed the glyphs and nothing else.
|
|
33
33
|
*/
|
|
34
|
-
export
|
|
34
|
+
export interface ParseDiffOptions {
|
|
35
|
+
/** Fold `-`/`+` pairs that differ only in whitespace back into context. */
|
|
36
|
+
ignoreWhitespace?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Which view the caller renders. Both views are built from one flush so a
|
|
39
|
+
* decision cannot land in one and not the other, but building the one nobody
|
|
40
|
+
* shows doubled the allocations on a full-context diff — an 80k-line file
|
|
41
|
+
* produced ~60k discarded `SplitDiffRow` objects. `'both'` is the default so
|
|
42
|
+
* the counts and the shared flush stay exactly as they were.
|
|
43
|
+
*/
|
|
44
|
+
views?: 'unified' | 'split' | 'both';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseUnifiedDiff(rawDiff: string, options: ParseDiffOptions = {}): {
|
|
35
48
|
lines: DiffLine[];
|
|
36
49
|
splitRows: SplitDiffRow[];
|
|
37
50
|
additions: number;
|
|
38
51
|
deletions: number;
|
|
39
52
|
} {
|
|
53
|
+
const ignoreWhitespace = options.ignoreWhitespace ?? false;
|
|
54
|
+
const views = options.views ?? 'both';
|
|
55
|
+
const wantUnified = views !== 'split';
|
|
56
|
+
const wantSplit = views !== 'unified';
|
|
40
57
|
const lines: DiffLine[] = [];
|
|
41
58
|
const splitRows: SplitDiffRow[] = [];
|
|
42
59
|
let additions = 0;
|
|
@@ -73,43 +90,47 @@ export function parseUnifiedDiff(rawDiff: string, ignoreWhitespace: boolean = fa
|
|
|
73
90
|
}
|
|
74
91
|
}
|
|
75
92
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
if (wantUnified) {
|
|
94
|
+
for (let i = 0; i < delBuffer.length; i++) {
|
|
95
|
+
const del = delBuffer[i];
|
|
96
|
+
const add = addBuffer[i];
|
|
97
|
+
if (folded[i] && add) {
|
|
98
|
+
// Show it as unchanged on both sides: the change is still in the file,
|
|
99
|
+
// the reader asked not to be shown formatting noise.
|
|
100
|
+
lines.push({
|
|
101
|
+
type: 'context',
|
|
102
|
+
// The new form, so the row reads as the file's current state.
|
|
103
|
+
text: add.text,
|
|
104
|
+
oldLineNumber: del.lineNumber,
|
|
105
|
+
newLineNumber: add.lineNumber,
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
lines.push({ type: 'del', text: del.text, oldLineNumber: del.lineNumber });
|
|
110
|
+
}
|
|
111
|
+
for (let i = 0; i < addBuffer.length; i++) {
|
|
112
|
+
const add = addBuffer[i];
|
|
113
|
+
if (folded[i]) continue;
|
|
114
|
+
lines.push({ type: 'add', text: add.text, newLineNumber: add.lineNumber });
|
|
90
115
|
}
|
|
91
|
-
lines.push({ type: 'del', text: del.text, oldLineNumber: del.lineNumber });
|
|
92
|
-
}
|
|
93
|
-
for (let i = 0; i < addBuffer.length; i++) {
|
|
94
|
-
const add = addBuffer[i];
|
|
95
|
-
if (folded[i]) continue;
|
|
96
|
-
lines.push({ type: 'add', text: add.text, newLineNumber: add.lineNumber });
|
|
97
116
|
}
|
|
98
117
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
if (wantSplit) {
|
|
119
|
+
for (let i = 0; i < maxLen; i++) {
|
|
120
|
+
const del = delBuffer[i];
|
|
121
|
+
const add = addBuffer[i];
|
|
122
|
+
if (folded[i] && del && add) {
|
|
123
|
+
splitRows.push({
|
|
124
|
+
left: { type: 'context', text: del.text, lineNumber: del.lineNumber },
|
|
125
|
+
right: { type: 'context', text: add.text, lineNumber: add.lineNumber },
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
103
129
|
splitRows.push({
|
|
104
|
-
left: { type: '
|
|
105
|
-
right: { type: '
|
|
130
|
+
left: del ? { type: 'del', text: del.text, lineNumber: del.lineNumber } : undefined,
|
|
131
|
+
right: add ? { type: 'add', text: add.text, lineNumber: add.lineNumber } : undefined,
|
|
106
132
|
});
|
|
107
|
-
continue;
|
|
108
133
|
}
|
|
109
|
-
splitRows.push({
|
|
110
|
-
left: del ? { type: 'del', text: del.text, lineNumber: del.lineNumber } : undefined,
|
|
111
|
-
right: add ? { type: 'add', text: add.text, lineNumber: add.lineNumber } : undefined,
|
|
112
|
-
});
|
|
113
134
|
}
|
|
114
135
|
delBuffer = [];
|
|
115
136
|
addBuffer = [];
|
|
@@ -156,16 +177,20 @@ export function parseUnifiedDiff(rawDiff: string, ignoreWhitespace: boolean = fa
|
|
|
156
177
|
} else {
|
|
157
178
|
flushBuffers();
|
|
158
179
|
const text = rawLine.startsWith(' ') ? rawLine.slice(1) : rawLine;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
180
|
+
if (wantUnified) {
|
|
181
|
+
lines.push({
|
|
182
|
+
type: 'context',
|
|
183
|
+
text,
|
|
184
|
+
oldLineNumber: oldCounter,
|
|
185
|
+
newLineNumber: newCounter,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (wantSplit) {
|
|
189
|
+
splitRows.push({
|
|
190
|
+
left: { type: 'context', text, lineNumber: oldCounter },
|
|
191
|
+
right: { type: 'context', text, lineNumber: newCounter },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
169
194
|
oldCounter++;
|
|
170
195
|
newCounter++;
|
|
171
196
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Covers `readSubagentProgressFrame`, the one reader every subagent-progress
|
|
8
|
+
* consumer goes through (sidebar roster, transcript hook, server liveness).
|
|
9
|
+
*
|
|
10
|
+
* The wrapper shape is the whole point: omp's `subagent_progress` frame nests
|
|
11
|
+
* the child's `AgentProgress` under `payload.progress` and puts NO `id` on the
|
|
12
|
+
* wrapper, so a reader that parses the wrapper alone yields an id-less object
|
|
13
|
+
* with none of the reported fields — silently, because every field is
|
|
14
|
+
* optional. The payload below is a verbatim frame captured from omp 18.3.2.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, expect, test } from 'bun:test';
|
|
18
|
+
|
|
19
|
+
import { parseSubagentProgress, readSubagentProgressFrame } from '@/shared/lib/omp/subagent/parse';
|
|
20
|
+
|
|
21
|
+
/** One `subagent_progress` frame as omp emits it (trimmed to the read fields). */
|
|
22
|
+
const REAL_FRAME = {
|
|
23
|
+
index: 0,
|
|
24
|
+
agent: 'scout',
|
|
25
|
+
agentSource: 'user',
|
|
26
|
+
task: 'Complete assignment thoroughly:\n\n# Target\nCount files.',
|
|
27
|
+
parentToolCallId: 'call_00_LsrwnITatwhVYX5NElPf2544',
|
|
28
|
+
detached: true,
|
|
29
|
+
assignment: '# Target\nCount files.',
|
|
30
|
+
sessionFile: '/tmp/sessions/parent/CountTsx.jsonl',
|
|
31
|
+
progress: {
|
|
32
|
+
index: 0,
|
|
33
|
+
id: 'CountTsx',
|
|
34
|
+
agent: 'scout',
|
|
35
|
+
agentSource: 'user',
|
|
36
|
+
status: 'running',
|
|
37
|
+
task: 'Complete assignment thoroughly:\n\n# Target\nCount files.',
|
|
38
|
+
recentTools: [],
|
|
39
|
+
recentOutput: [],
|
|
40
|
+
toolCount: 3,
|
|
41
|
+
requests: 2,
|
|
42
|
+
tokens: 10400,
|
|
43
|
+
cost: 0.003,
|
|
44
|
+
durationMs: 8700,
|
|
45
|
+
modelRole: 'smol',
|
|
46
|
+
resolvedModelIdentity: 'kenari/deepseek-v4-flash',
|
|
47
|
+
resolvedThinkingLevel: 'low',
|
|
48
|
+
resolvedModel: 'kenari/deepseek-v4-flash:low',
|
|
49
|
+
resolvedModelIsFallback: false,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
describe('readSubagentProgressFrame', () => {
|
|
54
|
+
test('unwraps the nested AgentProgress and keeps the fields it reports', () => {
|
|
55
|
+
const progress = readSubagentProgressFrame(REAL_FRAME);
|
|
56
|
+
expect(progress?.id).toBe('CountTsx');
|
|
57
|
+
expect(progress?.resolvedModel).toBe('kenari/deepseek-v4-flash:low');
|
|
58
|
+
expect(progress?.resolvedThinkingLevel).toBe('low');
|
|
59
|
+
expect(progress?.status).toBe('running');
|
|
60
|
+
expect(progress?.tokens).toBe(10400);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('reads a bare AgentProgress payload (snapshot field, not a frame)', () => {
|
|
64
|
+
const progress = readSubagentProgressFrame(REAL_FRAME.progress);
|
|
65
|
+
expect(progress?.id).toBe('CountTsx');
|
|
66
|
+
expect(progress?.resolvedThinkingLevel).toBe('low');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('keeps the wrapper id when the nested snapshot omits it', () => {
|
|
70
|
+
const progress = readSubagentProgressFrame({ id: 'Wrapped', progress: { status: 'running' } });
|
|
71
|
+
expect(progress?.id).toBe('Wrapped');
|
|
72
|
+
expect(progress?.status).toBe('running');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('returns undefined for a payload carrying no progress fields at all', () => {
|
|
76
|
+
expect(readSubagentProgressFrame({ event: { type: 'message_end' } })).toBeUndefined();
|
|
77
|
+
expect(readSubagentProgressFrame(null)).toBeUndefined();
|
|
78
|
+
expect(readSubagentProgressFrame('nonsense')).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('parseSubagentProgress', () => {
|
|
83
|
+
test('carries the resolved thinking level and drops a non-string one', () => {
|
|
84
|
+
expect(parseSubagentProgress({ resolvedThinkingLevel: 'max' })?.resolvedThinkingLevel).toBe('max');
|
|
85
|
+
expect(parseSubagentProgress({ resolvedThinkingLevel: null })?.resolvedThinkingLevel).toBeUndefined();
|
|
86
|
+
expect(parseSubagentProgress({ resolvedThinkingLevel: 3 })?.resolvedThinkingLevel).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -81,6 +81,8 @@ export function parseSubagentProgress(value: unknown): SubagentProgress | undefi
|
|
|
81
81
|
if (modelRole !== undefined) out.modelRole = modelRole;
|
|
82
82
|
const resolvedModel = asString(value.resolvedModel);
|
|
83
83
|
if (resolvedModel !== undefined) out.resolvedModel = resolvedModel;
|
|
84
|
+
const resolvedThinkingLevel = asString(value.resolvedThinkingLevel);
|
|
85
|
+
if (resolvedThinkingLevel !== undefined) out.resolvedThinkingLevel = resolvedThinkingLevel;
|
|
84
86
|
if (typeof value.resolvedModelIsFallback === 'boolean') out.resolvedModelIsFallback = value.resolvedModelIsFallback;
|
|
85
87
|
if (isRecord(value.retryState)) {
|
|
86
88
|
const attempt = asNumber(value.retryState.attempt);
|
|
@@ -180,6 +182,21 @@ export function parseSubagentLifecycle(value: unknown): SubagentInfo | undefined
|
|
|
180
182
|
return info;
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Read a progress frame's payload: `SubagentProgressPayload` nests the
|
|
187
|
+
* AgentProgress it reports under `progress`, while a bare AgentProgress (and a
|
|
188
|
+
* `get_subagents` snapshot's own field) is the object itself. Both shapes reach
|
|
189
|
+
* the same listeners, so unwrap here rather than at each call site — the outer
|
|
190
|
+
* wrapper's `id`/`index` fill in what the nested snapshot omits.
|
|
191
|
+
*/
|
|
192
|
+
export function readSubagentProgressFrame(payload: unknown): SubagentProgress | undefined {
|
|
193
|
+
const direct = parseSubagentProgress(payload);
|
|
194
|
+
if (!isRecord(payload) || !isRecord(payload.progress)) return direct;
|
|
195
|
+
const nested = parseSubagentProgress(payload.progress);
|
|
196
|
+
if (!nested) return direct;
|
|
197
|
+
return { ...nested, id: nested.id ?? direct?.id, index: nested.index ?? direct?.index };
|
|
198
|
+
}
|
|
199
|
+
|
|
183
200
|
/** Extract a compact live-activity entry from a subagent_event payload. */
|
|
184
201
|
export function parseSubagentActivityEvent(value: unknown): SubagentActivityEvent | null {
|
|
185
202
|
if (!isRecord(value)) return null;
|