ompchamber 3.1.1 → 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/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-qhzsybkn.js → index-1q6165r3.js} +641 -641
- package/package.json +1 -1
- 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-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/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/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/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-5yfre1kw.css +0 -1
|
@@ -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;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Covers the subagent transcript conversion's thinking-level stamping.
|
|
8
|
+
*
|
|
9
|
+
* A subagent transcript records its level as a separate `thinking_level_change`
|
|
10
|
+
* entry, never as a per-message field, so `convertMessages` has to walk the
|
|
11
|
+
* records and stamp the level in effect — otherwise the footer has no level to
|
|
12
|
+
* show and drops the segment entirely. The child's own level is the only
|
|
13
|
+
* correct one: a subagent routinely runs at a different level than the chat
|
|
14
|
+
* that spawned it, so borrowing the parent's would mislabel every row.
|
|
15
|
+
*
|
|
16
|
+
* Also pins the non-message entry handling the walk sits beside: a change
|
|
17
|
+
* record must be consumed, not converted into a row.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, expect, test } from 'bun:test';
|
|
21
|
+
|
|
22
|
+
import { convertMessages, mergeMessages } from '@/shared/lib/omp/subagent/transcript-client';
|
|
23
|
+
import type { ChatMessageData } from '@/shared/types';
|
|
24
|
+
|
|
25
|
+
/** An assistant message entry carrying text, as omp writes it to the JSONL. */
|
|
26
|
+
function assistantEntry(id: string, text: string): Record<string, unknown> {
|
|
27
|
+
return {
|
|
28
|
+
type: 'message',
|
|
29
|
+
id,
|
|
30
|
+
timestamp: 1790403839189,
|
|
31
|
+
message: {
|
|
32
|
+
role: 'assistant',
|
|
33
|
+
model: 'deepseek-v4-flash',
|
|
34
|
+
provider: 'kenari',
|
|
35
|
+
content: [{ type: 'text', text }],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function changeEntry(level: unknown): Record<string, unknown> {
|
|
41
|
+
return { type: 'thinking_level_change', id: `chg-${String(level)}`, thinkingLevel: level, configured: null };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('convertMessages thinking-level stamping', () => {
|
|
45
|
+
test('stamps the level in effect onto assistant rows', () => {
|
|
46
|
+
const rows = convertMessages([changeEntry('low'), assistantEntry('a1', 'first'), assistantEntry('a2', 'second')], false);
|
|
47
|
+
expect(rows.map((row) => row.thinkingLevel)).toEqual(['low', 'low']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('follows a mid-transcript change, so each turn keeps the level that served it', () => {
|
|
51
|
+
const rows = convertMessages([
|
|
52
|
+
changeEntry('low'),
|
|
53
|
+
assistantEntry('a1', 'first'),
|
|
54
|
+
changeEntry('high'),
|
|
55
|
+
assistantEntry('a2', 'second'),
|
|
56
|
+
], false);
|
|
57
|
+
expect(rows[0].thinkingLevel).toBe('low');
|
|
58
|
+
expect(rows[1].thinkingLevel).toBe('high');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('normalizes a null level record to "off" rather than dropping the segment', () => {
|
|
62
|
+
const rows = convertMessages([changeEntry(null), assistantEntry('a1', 'first')], false);
|
|
63
|
+
expect(rows[0].thinkingLevel).toBe('off');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('leaves user rows unstamped and the level absent before any record', () => {
|
|
67
|
+
const rows = convertMessages([
|
|
68
|
+
{ type: 'message', id: 'u1', message: { role: 'user', content: 'hi' } },
|
|
69
|
+
assistantEntry('a1', 'answer'),
|
|
70
|
+
changeEntry('max'),
|
|
71
|
+
assistantEntry('a2', 'later'),
|
|
72
|
+
], false);
|
|
73
|
+
expect(rows[0].role).toBe('user');
|
|
74
|
+
expect(rows[0].thinkingLevel).toBeUndefined();
|
|
75
|
+
expect(rows[1].thinkingLevel).toBeUndefined();
|
|
76
|
+
expect(rows[2].thinkingLevel).toBe('max');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('consumes the change record instead of turning it into a row', () => {
|
|
80
|
+
const rows = convertMessages([changeEntry('low')], false);
|
|
81
|
+
expect(rows).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('mergeMessages', () => {
|
|
86
|
+
test('replaces a streamed row in place so its stamped level survives finalization', () => {
|
|
87
|
+
const streamed = convertMessages([changeEntry('low'), assistantEntry('a1', 'partial')], true);
|
|
88
|
+
const final = convertMessages([changeEntry('low'), assistantEntry('a1', 'complete answer')], false);
|
|
89
|
+
const merged = mergeMessages(streamed, final);
|
|
90
|
+
expect(merged).toHaveLength(1);
|
|
91
|
+
expect(merged[0].content).toBe('complete answer');
|
|
92
|
+
expect(merged[0].thinkingLevel).toBe('low');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('appends a row whose id is new', () => {
|
|
96
|
+
const prev: ChatMessageData[] = convertMessages([changeEntry('low'), assistantEntry('a1', 'first')], false);
|
|
97
|
+
const merged = mergeMessages(prev, convertMessages([changeEntry('low'), assistantEntry('a2', 'second')], false));
|
|
98
|
+
expect(merged.map((row) => row.id)).toEqual(['a1', 'a2']);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { ChatMessageData, SubagentMessagesPage } from '@/shared/types';
|
|
14
14
|
import { toChatMessage } from '@/shared/lib/omp/session/mapper';
|
|
15
15
|
import { extractText } from '@/shared/lib/omp/session/parse-message-blocks';
|
|
16
|
+
import { normalizeThinkingLevel } from '@/shared/lib/models/thinking-levels';
|
|
16
17
|
import { isRecord } from '@/shared/lib/util/guards';
|
|
17
18
|
|
|
18
19
|
/** Normalize one wire message: the RPC may hand back bare omp AgentMessages
|
|
@@ -29,9 +30,20 @@ function normalizeRawMessage(value: unknown): Record<string, unknown> | null {
|
|
|
29
30
|
|
|
30
31
|
export function convertMessages(raw: unknown[], streaming: boolean): ChatMessageData[] {
|
|
31
32
|
const out: ChatMessageData[] = [];
|
|
33
|
+
// omp records the thinking level as its own `thinking_level_change` entry
|
|
34
|
+
// (session state), not as a per-message field. Walk the records in order and
|
|
35
|
+
// stamp the level in effect onto each non-user turn, so a subagent footer
|
|
36
|
+
// shows the level that served that turn — the child's own level, never the
|
|
37
|
+
// parent session's. Mirrors the server's main-session loader
|
|
38
|
+
// (`loadSessionMessages`).
|
|
39
|
+
let currentLevel: string | undefined;
|
|
32
40
|
for (const item of raw) {
|
|
33
41
|
if (isRecord(item)) {
|
|
34
42
|
const entryType = typeof item.type === 'string' ? item.type : '';
|
|
43
|
+
if (entryType === 'thinking_level_change') {
|
|
44
|
+
currentLevel = normalizeThinkingLevel(item.thinkingLevel);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
35
47
|
if (entryType === 'custom' || entryType === 'custom_message') {
|
|
36
48
|
const customType = typeof item.customType === 'string' ? item.customType : '';
|
|
37
49
|
if (customType === 'session_exit') {
|
|
@@ -77,7 +89,11 @@ export function convertMessages(raw: unknown[], streaming: boolean): ChatMessage
|
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
const converted = toChatMessage(record, streaming);
|
|
80
|
-
if (converted)
|
|
92
|
+
if (!converted) continue;
|
|
93
|
+
if (currentLevel !== undefined && converted.role !== 'user') {
|
|
94
|
+
converted.thinkingLevel = currentLevel;
|
|
95
|
+
}
|
|
96
|
+
out.push(converted);
|
|
81
97
|
}
|
|
82
98
|
return out;
|
|
83
99
|
}
|
|
@@ -49,6 +49,12 @@ export interface SubagentProgress {
|
|
|
49
49
|
modelOverride?: string | string[];
|
|
50
50
|
modelRole?: string;
|
|
51
51
|
resolvedModel?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Thinking level the child's own model resolved to. Never inferred from the
|
|
54
|
+
* parent session: the child may run at a different level than the chat that
|
|
55
|
+
* spawned it, so the footer must read this one or show nothing.
|
|
56
|
+
*/
|
|
57
|
+
resolvedThinkingLevel?: string;
|
|
52
58
|
resolvedModelIsFallback?: boolean;
|
|
53
59
|
retryState?: SubagentRetryState;
|
|
54
60
|
retryFailure?: { attempt: number; errorMessage: string };
|