@kabmo-labs/kabmo 0.5.3 → 0.6.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 +359 -350
- package/dist/client/static/js/{index-9js9mrmj.js → index-ae9z0yvm.js} +704 -704
- package/dist/client/static/js/index-nzyjf92m.css +1 -0
- package/marketplace/plugins/stats/README.md +26 -4
- package/marketplace/plugins/stats/package.json +2 -2
- package/marketplace/plugins/stats/src/HeaderReadout.test.tsx +157 -0
- package/marketplace/plugins/stats/src/HeaderReadout.tsx +115 -0
- package/marketplace/plugins/stats/src/app.tsx +11 -0
- package/marketplace/plugins/stats/src/format.test.ts +27 -1
- package/marketplace/plugins/stats/src/format.ts +41 -0
- package/package.json +2 -2
- package/src/client/bootstrap.d.ts +11 -0
- package/src/client/components/common/header-controls/MoreMenu.tsx +38 -1
- package/src/client/components/common/header-controls/index.tsx +9 -2
- package/src/client/components/common/header-controls/plugin-entry.test.tsx +229 -0
- package/src/client/components/common/header-panel-buttons/entries.test.ts +46 -0
- package/src/client/components/common/header-panel-buttons/entries.ts +109 -0
- package/src/client/components/common/header-panel-buttons/index.tsx +71 -38
- package/src/client/components/common/server-switcher/index.test.tsx +150 -0
- package/src/client/components/common/server-switcher/index.tsx +281 -0
- package/src/client/components/common/window-controls/index.test.tsx +113 -0
- package/src/client/components/common/window-controls/index.tsx +66 -0
- package/src/client/components/layout/desktop-layout/LeftColumn.tsx +3 -6
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +59 -3
- package/src/client/components/layout/desktop-layout/index.tsx +34 -26
- package/src/client/components/layout/session-sidebar/Footer.tsx +12 -12
- package/src/client/components/layout/session-sidebar/Header.tsx +4 -2
- package/src/client/components/mobile/LayoutWrapper.tsx +36 -0
- package/src/client/components/mobile/RightSidebar.tsx +2 -2
- package/src/client/components/mobile/SessionSidebar.tsx +5 -8
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +15 -12
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +9 -2
- package/src/client/components/mobile/mobile-session-sidebar/folder-gap.test.ts +41 -0
- package/src/client/components/mobile/mobile-session-sidebar/folder-gap.ts +39 -0
- package/src/client/components/mobile/mobile-session-sidebar/use-dock.ts +6 -2
- package/src/client/components/settings/categories/OmpSettings.tsx +25 -29
- package/src/client/components/settings/categories/omp-settings/PresetBar.tsx +187 -0
- package/src/client/components/settings/categories/omp-settings/RoleEditor.test.ts +260 -32
- package/src/client/components/settings/categories/omp-settings/RoleEditor.tsx +198 -102
- package/src/client/components/settings/categories/omp-settings/RoleRow.tsx +79 -0
- package/src/client/data/settings/omp-schema.json +2 -2
- package/src/client/hooks/desktop/servers.test.ts +73 -0
- package/src/client/hooks/desktop/servers.ts +126 -0
- package/src/client/hooks/settings/model-roles.ts +156 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +157 -0
- package/src/client/hooks/ui/updates/shell-download.ts +220 -0
- package/src/client/hooks/workspace/keybindings.ts +44 -0
- package/src/client/hooks/workspace/open-panel.ts +42 -0
- package/src/client/lib/desktop/bridge.ts +158 -0
- package/src/client/lib/desktop/titlebar.test.ts +99 -0
- package/src/client/lib/desktop/titlebar.ts +97 -0
- package/src/client/lib/plugins/slots.test.ts +54 -0
- package/src/client/lib/plugins/slots.ts +37 -1
- package/src/server/db.server.test.ts +44 -3
- package/src/server/db.server.ts +26 -2
- package/src/server/lib/bundler/titlebar-css.test.ts +75 -0
- package/src/server/lib/db/schema.ts +3 -0
- package/src/server/lib/model-roles/catalog.server.test.ts +75 -0
- package/src/server/lib/model-roles/catalog.server.ts +112 -0
- package/src/server/lib/model-roles/mock.server.ts +40 -0
- package/src/server/lib/model-roles/schema.server.ts +36 -0
- package/src/server/lib/model-roles/store.server.test.ts +218 -0
- package/src/server/lib/model-roles/store.server.ts +188 -0
- package/src/server/lib/omp/rpc/destroy-stream-row.test.ts +4 -4
- package/src/server/lib/omp/rpc/frame-fold.test.ts +30 -1
- package/src/server/lib/omp/rpc/frame-fold.ts +6 -1
- package/src/server/lib/omp/rpc/manager.ts +18 -13
- package/src/server/lib/omp/rpc/session-command-guards.ts +4 -0
- package/src/server/lib/omp/rpc/session-commands.test.ts +24 -0
- package/src/server/lib/omp/rpc/session-commands.ts +7 -0
- package/src/server/lib/omp/session/auto-title.server.test.ts +147 -93
- package/src/server/lib/omp/session/auto-title.server.ts +150 -120
- package/src/server/lib/omp/session/rename-with-ai.server.test.ts +18 -6
- package/src/server/lib/omp/session/rename-with-ai.server.ts +36 -20
- package/src/server/lib/omp/session/title-model-overlay.server.test.ts +29 -14
- package/src/server/lib/omp/session/title-model-overlay.server.ts +46 -25
- package/src/server/lib/omp/session/title-request.server.test.ts +183 -0
- package/src/server/lib/omp/session/title-request.server.ts +213 -0
- package/src/server/lib/panels/baseline.server.test.ts +4 -4
- package/src/server/lib/panels/compat.server.test.ts +4 -4
- package/src/server/lib/panels/registry-bundled.server.test.ts +4 -4
- package/src/server/lib/panels/registry.server.test.ts +4 -4
- package/src/server/lib/panels/state.server.test.ts +4 -4
- package/src/server/lib/panels/sync.server.test.ts +4 -4
- package/src/server/lib/plugins/background.test.ts +2 -2
- package/src/server/lib/plugins/e2e.test.ts +2 -2
- package/src/server/lib/plugins/platform.test.ts +2 -2
- package/src/server/lib/queue/delivery-dispatch.server.test.ts +34 -0
- package/src/server/lib/queue/delivery.server.ts +11 -2
- package/src/server/lib/updates/shell-release.test.ts +184 -0
- package/src/server/lib/updates/shell-release.ts +215 -0
- package/src/server/lib/waiting/store.server.test.ts +2 -2
- package/src/server/plugins/ssr.ts +13 -10
- package/src/server/routes/chat/stream.test.ts +4 -4
- package/src/server/routes/models/roles.ts +73 -14
- package/src/server/routes/panels/css.test.ts +4 -4
- package/src/server/routes/panels/panel-routes.test-util.ts +3 -3
- package/src/server/routes/plugins/routes.test.ts +4 -4
- package/src/server/routes/sessions/batch-delete.test.ts +4 -4
- package/src/server/routes/updates/android.ts +2 -2
- package/src/server/routes/updates/index.ts +2 -0
- package/src/server/routes/updates/ios.ts +26 -0
- package/src/server/routes/waiting/routes.test.ts +2 -2
- package/src/shared/lib/models/server-cache.ts +11 -0
- package/src/shared/lib/omp/config/model-roles.ts +129 -0
- package/src/shared/lib/updates/shell-ua.test.ts +56 -24
- package/src/shared/lib/updates/shell-ua.ts +41 -20
- package/src/shared/lib/workspace/panel-open.test.ts +56 -0
- package/src/shared/lib/workspace/panel-open.ts +76 -0
- package/src/shared/types/model-roles.ts +54 -0
- package/src/test-support/isolated-db.ts +17 -5
- package/dist/client/static/js/index-3qamd33d.css +0 -1
- package/src/client/hooks/ui/updates/android-download.test.ts +0 -112
- package/src/client/hooks/ui/updates/android-download.ts +0 -143
- package/src/server/lib/updates/android-release.test.ts +0 -138
- package/src/server/lib/updates/android-release.ts +0 -183
|
@@ -3,51 +3,102 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Auto-titling's two properties that must never regress.
|
|
8
|
+
*
|
|
9
|
+
* 1. **It never overwrites a name the operator set.** Kabmo fires omp's own
|
|
10
|
+
* `/rename` (which persists through `setSessionName(title, "user")`) in the
|
|
11
|
+
* background, and that write has no "don't overwrite" guard of its own — omp
|
|
12
|
+
* only refuses an `"auto"` write over a `"user"` title. The decision to skip
|
|
13
|
+
* a NAMED session is ours, and a bug here silently destroys a name the
|
|
14
|
+
* operator typed.
|
|
15
|
+
*
|
|
16
|
+
* 2. **The run end ALWAYS asks, and it asks on the session's own model.** Both
|
|
17
|
+
* halves were broken on a real install and neither was visible from Kabmo's
|
|
18
|
+
* side: the fallback stood down while an early generation was "in flight"
|
|
19
|
+
* (it had in fact already failed 52 s earlier), and the request ran on omp's
|
|
20
|
+
* configured `tiny` role — `kenari/deepseek-v4-flash` here, whose plan had
|
|
21
|
+
* lapsed — so every attempt answered `429 plan_limit_reached` and no session
|
|
22
|
+
* was ever named. Six measured attempts, four unnamed sessions.
|
|
23
|
+
*
|
|
24
|
+
* The fake `get_state` and the recorded command list are the whole surface: no
|
|
25
|
+
* omp child, no database, no wall-clock waiting.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
29
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
30
|
+
import { tmpdir } from 'node:os';
|
|
31
|
+
import { join } from 'node:path';
|
|
7
32
|
|
|
8
33
|
import {
|
|
34
|
+
awaitTitleDispatch,
|
|
9
35
|
consumeAutoTitleOutput,
|
|
10
36
|
markTitleRequestSent,
|
|
11
37
|
triggerAutoSessionTitle,
|
|
12
38
|
type AutoTitleHost,
|
|
13
39
|
} from '@/server/lib/omp/session/auto-title.server';
|
|
40
|
+
import { titleModelOverlayPath } from '@/server/lib/omp/session/title-model-overlay.server';
|
|
14
41
|
|
|
15
42
|
/**
|
|
16
|
-
* The
|
|
17
|
-
*
|
|
18
|
-
* `setSessionName(title, "user")`) in the background, and that write has no
|
|
19
|
-
* "don't overwrite" guard of its own — omp only refuses an `"auto"` write over
|
|
20
|
-
* a `"user"` title. So the decision to skip a NAMED session is ours, and a bug
|
|
21
|
-
* here silently destroys a name the operator typed.
|
|
22
|
-
*
|
|
23
|
-
* These tests drive the guard directly: a fake `get_state` reports a name, and
|
|
24
|
-
* the assertion is that no `/rename` reaches the wire.
|
|
43
|
+
* The model pin is a FILE under the agent dir, so the suite points that dir at
|
|
44
|
+
* a temp tree: nothing here may touch the developer's own `~/.omp`.
|
|
25
45
|
*/
|
|
46
|
+
let agentDir: string;
|
|
47
|
+
let previousAgentDir: string | undefined;
|
|
48
|
+
|
|
49
|
+
beforeAll(() => {
|
|
50
|
+
agentDir = mkdtempSync(join(tmpdir(), 'kabmo-auto-title-'));
|
|
51
|
+
previousAgentDir = Bun.env.PI_CODING_AGENT_DIR;
|
|
52
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterAll(() => {
|
|
56
|
+
if (previousAgentDir === undefined) delete Bun.env.PI_CODING_AGENT_DIR;
|
|
57
|
+
else process.env.PI_CODING_AGENT_DIR = previousAgentDir;
|
|
58
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
59
|
+
});
|
|
26
60
|
|
|
27
61
|
interface FakeProc {
|
|
28
62
|
commands: Array<Record<string, unknown>>;
|
|
29
63
|
sendCommand<T>(command: { type: string; [key: string]: unknown }): Promise<T>;
|
|
30
64
|
}
|
|
31
65
|
|
|
66
|
+
/** Hosts built by the current test. The model pin is ONE file (newest wins), so
|
|
67
|
+
* a request left unanswered would keep it pinned for every later test — the
|
|
68
|
+
* drain below is what makes each case independent. */
|
|
69
|
+
const created: AutoTitleHost[] = [];
|
|
70
|
+
|
|
71
|
+
afterEach(async () => {
|
|
72
|
+
for (const host of created.splice(0)) {
|
|
73
|
+
while (host.autoTitleClaims > 0) consumeAutoTitleOutput(host);
|
|
74
|
+
}
|
|
75
|
+
// Let the pin's `finally` (an atomic file replace) run.
|
|
76
|
+
for (let i = 0; i < 16; i += 1) await Promise.resolve();
|
|
77
|
+
});
|
|
78
|
+
|
|
32
79
|
function makeHost(options: {
|
|
33
80
|
sessionName?: string;
|
|
34
81
|
sessionId?: string;
|
|
35
|
-
inFlight?: boolean;
|
|
36
82
|
pending?: boolean;
|
|
37
83
|
requested?: boolean;
|
|
84
|
+
claims?: number;
|
|
38
85
|
windowUntil?: number;
|
|
39
|
-
|
|
40
|
-
|
|
86
|
+
model?: { provider: string; modelId: string } | null;
|
|
87
|
+
/** Called the moment a `/rename` prompt reaches the wire. */
|
|
41
88
|
onPrompt?: (host: AutoTitleHost) => void;
|
|
42
89
|
/** Make the `/rename` send reject, for the rollback path. */
|
|
43
90
|
failPrompt?: boolean;
|
|
91
|
+
/** Suppress the frame that answers a `/rename`, so its claim stays open. */
|
|
92
|
+
unanswered?: boolean;
|
|
44
93
|
}): { host: AutoTitleHost; proc: FakeProc } {
|
|
45
94
|
const host: AutoTitleHost = {
|
|
46
95
|
sessionId: 'sess-1',
|
|
47
|
-
autoTitleInFlight: options.inFlight ?? false,
|
|
48
96
|
autoTitleWindowUntil: options.windowUntil ?? 0,
|
|
97
|
+
autoTitleClaims: options.claims ?? 0,
|
|
98
|
+
autoTitleDispatch: null,
|
|
49
99
|
autoTitlePending: options.pending ?? true,
|
|
50
100
|
autoTitleRequested: options.requested ?? false,
|
|
101
|
+
autoTitleModel: options.model ?? { provider: 'kenari-jatis', modelId: 'deepseek-v4-1-flash' },
|
|
51
102
|
proc: undefined as unknown as AutoTitleHost['proc'],
|
|
52
103
|
};
|
|
53
104
|
const proc: FakeProc = {
|
|
@@ -62,10 +113,14 @@ function makeHost(options: {
|
|
|
62
113
|
}
|
|
63
114
|
options.onPrompt?.(host);
|
|
64
115
|
if (options.failPrompt) throw new Error('send failed');
|
|
116
|
+
// The generation is reported by frames the fold delivers; a test that
|
|
117
|
+
// wants the claim consumed drives `consumeAutoTitleOutput` itself.
|
|
118
|
+
if (!options.unanswered) consumeAutoTitleOutput(host);
|
|
65
119
|
return undefined as T;
|
|
66
120
|
},
|
|
67
121
|
};
|
|
68
122
|
host.proc = proc as unknown as AutoTitleHost['proc'];
|
|
123
|
+
created.push(host);
|
|
69
124
|
return { host, proc };
|
|
70
125
|
}
|
|
71
126
|
|
|
@@ -82,50 +137,42 @@ describe('triggerAutoSessionTitle', () => {
|
|
|
82
137
|
const renames = sentRenames(proc);
|
|
83
138
|
expect(renames).toHaveLength(1);
|
|
84
139
|
expect(renames[0].message).toBe('/rename');
|
|
85
|
-
// The request window opens BEFORE the command is sent, so the
|
|
86
|
-
// `prompt_result {agentInvoked:false}` omp answers within the same tick is
|
|
87
|
-
// already attributable to us when it arrives.
|
|
88
|
-
expect(host.autoTitleWindowUntil).toBeGreaterThan(Date.now());
|
|
89
140
|
});
|
|
90
141
|
|
|
91
|
-
test('the request
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
142
|
+
test('the request claim is armed BEFORE the /rename is sent', async () => {
|
|
143
|
+
// omp answers the `/rename` prompt with `prompt_result {agentInvoked:false}`
|
|
144
|
+
// in the same tick (measured 3 ms) while the operator's turn still streams.
|
|
145
|
+
// The fold suppresses that frame only while a claim is armed.
|
|
146
|
+
let claimsAtSend = -1;
|
|
147
|
+
const { host, proc } = makeHost({
|
|
148
|
+
unanswered: true,
|
|
149
|
+
onPrompt: (h) => {
|
|
150
|
+
claimsAtSend = h.autoTitleClaims;
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
void triggerAutoSessionTitle(host, 'opening');
|
|
154
|
+
await awaitTitleDispatch(host);
|
|
103
155
|
|
|
104
156
|
expect(sentRenames(proc)).toHaveLength(1);
|
|
105
|
-
expect(
|
|
157
|
+
expect(claimsAtSend).toBe(1);
|
|
106
158
|
});
|
|
107
159
|
|
|
108
|
-
test('rolls the
|
|
109
|
-
// An armed window with no request behind it would swallow the next
|
|
110
|
-
// operator frame inside its 60 s span — and block the settle-stage
|
|
111
|
-
// fallback, which stands down while a generation is "in flight".
|
|
160
|
+
test('rolls the claim back when the /rename never reached omp', async () => {
|
|
112
161
|
const { host, proc } = makeHost({ failPrompt: true });
|
|
113
162
|
await triggerAutoSessionTitle(host, 'opening');
|
|
114
163
|
|
|
115
164
|
expect(sentRenames(proc)).toHaveLength(1);
|
|
165
|
+
expect(host.autoTitleClaims).toBe(0);
|
|
116
166
|
expect(host.autoTitleWindowUntil).toBe(0);
|
|
117
167
|
});
|
|
118
168
|
|
|
119
169
|
test('never overwrites a name the operator set', async () => {
|
|
120
|
-
// At either stage: the name is re-read from omp, so a rename made in
|
|
121
|
-
// another tab survives both the early attempt and the fallback.
|
|
122
170
|
for (const stage of ['opening', 'settle'] as const) {
|
|
123
171
|
const { host, proc } = makeHost({ sessionName: 'My Deliberate Name' });
|
|
124
172
|
await triggerAutoSessionTitle(host, stage);
|
|
125
173
|
|
|
126
174
|
expect(sentRenames(proc)).toHaveLength(0);
|
|
127
|
-
|
|
128
|
-
expect(host.autoTitleWindowUntil).toBe(0);
|
|
175
|
+
expect(host.autoTitleClaims).toBe(0);
|
|
129
176
|
}
|
|
130
177
|
});
|
|
131
178
|
|
|
@@ -137,27 +184,13 @@ describe('triggerAutoSessionTitle', () => {
|
|
|
137
184
|
});
|
|
138
185
|
|
|
139
186
|
test('skips when the session changed under the read', async () => {
|
|
140
|
-
// `get_state` answered for a different conversation (a reset or switch
|
|
141
|
-
// between the trigger and the read) — titling this session would name the
|
|
142
|
-
// wrong one.
|
|
143
187
|
const { host, proc } = makeHost({ sessionId: 'someone-else' });
|
|
144
188
|
await triggerAutoSessionTitle(host, 'opening');
|
|
145
189
|
|
|
146
190
|
expect(sentRenames(proc)).toHaveLength(0);
|
|
147
191
|
});
|
|
148
192
|
|
|
149
|
-
test('does not stack a second request while one is in flight', async () => {
|
|
150
|
-
const { host, proc } = makeHost({ inFlight: true });
|
|
151
|
-
await triggerAutoSessionTitle(host, 'opening');
|
|
152
|
-
|
|
153
|
-
expect(sentRenames(proc)).toHaveLength(0);
|
|
154
|
-
expect(proc.commands).toHaveLength(0);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
193
|
test('the early attempt does not consume the eligibility latch', async () => {
|
|
158
|
-
// The fallback exists precisely because the early attempt can come back
|
|
159
|
-
// empty. If `opening` consumed the latch, a provider error at the first
|
|
160
|
-
// user message would leave the session unnamed for good.
|
|
161
194
|
const { host } = makeHost({});
|
|
162
195
|
await triggerAutoSessionTitle(host, 'opening');
|
|
163
196
|
|
|
@@ -165,39 +198,45 @@ describe('triggerAutoSessionTitle', () => {
|
|
|
165
198
|
expect(host.autoTitleRequested).toBe(true);
|
|
166
199
|
});
|
|
167
200
|
|
|
168
|
-
test('the
|
|
169
|
-
// The
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
201
|
+
test('the run end ALWAYS asks, even while an early attempt is unanswered', async () => {
|
|
202
|
+
// The shipped defect: the fallback stood down while the early generation's
|
|
203
|
+
// window was still armed, on the assumption that generation would land. On
|
|
204
|
+
// a real session it had already failed ~52 s earlier, so the run ended with
|
|
205
|
+
// nothing asked again and the session kept its placeholder forever.
|
|
206
|
+
const { host, proc } = makeHost({ unanswered: true });
|
|
207
|
+
void triggerAutoSessionTitle(host, 'opening');
|
|
208
|
+
await awaitTitleDispatch(host);
|
|
209
|
+
expect(sentRenames(proc)).toHaveLength(1);
|
|
210
|
+
expect(host.autoTitleClaims).toBe(1);
|
|
175
211
|
|
|
176
|
-
|
|
212
|
+
void triggerAutoSessionTitle(host, 'settle');
|
|
213
|
+
await awaitTitleDispatch(host);
|
|
177
214
|
|
|
178
215
|
expect(sentRenames(proc)).toHaveLength(2);
|
|
216
|
+
// Consumed: this conversation's last chance has been taken.
|
|
179
217
|
expect(host.autoTitlePending).toBe(false);
|
|
180
218
|
});
|
|
181
219
|
|
|
182
|
-
test('the fallback
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
const { host
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expect(
|
|
220
|
+
test('the fallback is dispatched only after the early attempt acknowledged', async () => {
|
|
221
|
+
// Two frames on one stdin: omp cancels a superseded rename and emits no
|
|
222
|
+
// frame for it, so the two dispatches must not race.
|
|
223
|
+
const order: string[] = [];
|
|
224
|
+
const { host } = makeHost({
|
|
225
|
+
unanswered: true,
|
|
226
|
+
onPrompt: () => order.push(`send:${order.length}`),
|
|
227
|
+
});
|
|
228
|
+
void triggerAutoSessionTitle(host, 'opening');
|
|
229
|
+
await awaitTitleDispatch(host);
|
|
230
|
+
void triggerAutoSessionTitle(host, 'settle');
|
|
231
|
+
await awaitTitleDispatch(host);
|
|
232
|
+
|
|
233
|
+
expect(order).toHaveLength(2);
|
|
234
|
+
// Both were dispatched, and the second only after the first resolved its
|
|
235
|
+
// ack — the chain is what guarantees the ordering.
|
|
236
|
+
expect(host.autoTitleClaims).toBe(2);
|
|
196
237
|
});
|
|
197
238
|
|
|
198
239
|
test('fires the early attempt once per run', async () => {
|
|
199
|
-
// A queued steer message also settles as a user message inside the same
|
|
200
|
-
// run; re-asking on it would cancel the first generation for no gain.
|
|
201
240
|
const { host, proc } = makeHost({});
|
|
202
241
|
await triggerAutoSessionTitle(host, 'opening');
|
|
203
242
|
await triggerAutoSessionTitle(host, 'opening');
|
|
@@ -206,42 +245,59 @@ describe('triggerAutoSessionTitle', () => {
|
|
|
206
245
|
});
|
|
207
246
|
|
|
208
247
|
test('titles from the first run only', async () => {
|
|
209
|
-
// The regression this latch exists for: a second turn used to fire its own
|
|
210
|
-
// `/rename`, and omp derives that title from the NEWEST turns — so the
|
|
211
|
-
// session ended up named after message two.
|
|
212
248
|
const { host, proc } = makeHost({});
|
|
213
249
|
await triggerAutoSessionTitle(host, 'opening');
|
|
214
250
|
await triggerAutoSessionTitle(host, 'settle');
|
|
215
|
-
expect(sentRenames(proc)).toHaveLength(
|
|
251
|
+
expect(sentRenames(proc)).toHaveLength(2);
|
|
216
252
|
|
|
217
|
-
// A later run reaches neither stage with the latch consumed.
|
|
218
253
|
await triggerAutoSessionTitle(host, 'opening');
|
|
219
254
|
await triggerAutoSessionTitle(host, 'settle');
|
|
220
255
|
|
|
221
|
-
expect(sentRenames(proc)).toHaveLength(
|
|
256
|
+
expect(sentRenames(proc)).toHaveLength(2);
|
|
222
257
|
});
|
|
223
258
|
|
|
224
259
|
test('skips a conversation that already has a first turn behind it', async () => {
|
|
225
|
-
// A child resumed for an existing session (`--resume` after the idle
|
|
226
|
-
// reclaim) reports a non-zero message count, so its next turn must not
|
|
227
|
-
// name it — the session is either titled already or deliberately unnamed.
|
|
228
260
|
const { host, proc } = makeHost({ pending: false });
|
|
229
261
|
await triggerAutoSessionTitle(host, 'opening');
|
|
230
262
|
await triggerAutoSessionTitle(host, 'settle');
|
|
231
263
|
|
|
232
264
|
expect(sentRenames(proc)).toHaveLength(0);
|
|
233
|
-
expect(host.
|
|
265
|
+
expect(host.autoTitleClaims).toBe(0);
|
|
234
266
|
});
|
|
235
267
|
|
|
236
268
|
test('consumes the latch even when generation is skipped', async () => {
|
|
237
|
-
// A declined generation must not leave the door open for the next turn:
|
|
238
|
-
// otherwise a first turn whose title the provider refused gets re-asked
|
|
239
|
-
// after every later message, which is the same bug one step later.
|
|
240
269
|
const { host } = makeHost({});
|
|
241
270
|
await triggerAutoSessionTitle(host, 'settle');
|
|
242
271
|
|
|
243
272
|
expect(host.autoTitlePending).toBe(false);
|
|
244
273
|
});
|
|
274
|
+
|
|
275
|
+
test('runs the request on the SESSION model, not omp tiny role', async () => {
|
|
276
|
+
// The regression: every attempt ran on omp's configured `tiny` role, whose
|
|
277
|
+
// plan had lapsed, so the generation answered 429 and no session was ever
|
|
278
|
+
// named. The pin is a file omp watches, so its content at send time is the
|
|
279
|
+
// observable proof of which role the request runs under.
|
|
280
|
+
let pinned = '';
|
|
281
|
+
const { host } = makeHost({
|
|
282
|
+
unanswered: true,
|
|
283
|
+
onPrompt: () => {
|
|
284
|
+
pinned = readFileSync(titleModelOverlayPath(), 'utf8');
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
void triggerAutoSessionTitle(host, 'opening');
|
|
288
|
+
await awaitTitleDispatch(host);
|
|
289
|
+
|
|
290
|
+
expect(pinned).toContain('tiny: "kenari-jatis/deepseek-v4-1-flash"');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('releases the pin once the request has answered', async () => {
|
|
294
|
+
const { host } = makeHost({});
|
|
295
|
+
await triggerAutoSessionTitle(host, 'opening');
|
|
296
|
+
|
|
297
|
+
// Restored to the no-op overlay, so the next child that loads it is not
|
|
298
|
+
// pinned to a session's model.
|
|
299
|
+
expect(readFileSync(titleModelOverlayPath(), 'utf8')).toBe('{}\n');
|
|
300
|
+
});
|
|
245
301
|
});
|
|
246
302
|
|
|
247
303
|
describe('auto-title output window', () => {
|
|
@@ -250,8 +306,6 @@ describe('auto-title output window', () => {
|
|
|
250
306
|
markTitleRequestSent(host);
|
|
251
307
|
|
|
252
308
|
expect(consumeAutoTitleOutput(host)).toBe(true);
|
|
253
|
-
// omp emits a single command_output per /rename; a second frame inside the
|
|
254
|
-
// window belongs to something the operator typed and must reach the UI.
|
|
255
309
|
expect(consumeAutoTitleOutput(host)).toBe(false);
|
|
256
310
|
});
|
|
257
311
|
|
|
@@ -260,7 +314,7 @@ describe('auto-title output window', () => {
|
|
|
260
314
|
expect(consumeAutoTitleOutput(host)).toBe(false);
|
|
261
315
|
});
|
|
262
316
|
|
|
263
|
-
test('expires so a stale
|
|
317
|
+
test('expires so a stale claim cannot swallow an operator command', () => {
|
|
264
318
|
const { host } = makeHost({});
|
|
265
319
|
markTitleRequestSent(host);
|
|
266
320
|
host.autoTitleWindowUntil = Date.now() - 1;
|