ompchamber 0.8.0 → 1.1.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/static/css/index.b859cc1cc5.css +2 -0
- package/dist/client/static/js/async/{5b.0d6ae50fe8.js → 5b.a5ccbc19dc.js} +2 -2
- package/dist/client/static/js/async/g.a06ca3b3e9.js +2 -0
- package/dist/client/static/js/async/{m.0896dab823.js → m.c038c71b9f.js} +2 -2
- package/dist/client/static/js/async/ms.98960af4eb.js +67 -0
- package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +12 -0
- package/dist/client/static/js/async/ot.84c71b308f.js +2 -0
- package/dist/client/static/js/async/{x4.69ec69c61b.js.LICENSE.txt → ot.84c71b308f.js.LICENSE.txt} +5 -0
- package/dist/client/static/js/index.6e065017eb.js +33 -0
- package/package.json +2 -1
- package/src/client/components/common/ComposerPicker.tsx +16 -4
- package/src/client/components/common/ComposerTextarea.tsx +1 -0
- package/src/client/components/layout/RightActivityBar.tsx +1 -1
- package/src/client/components/layout/desktop-layout/ResizeHandle.tsx +2 -2
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +13 -2
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +107 -56
- package/src/client/components/layout/desktop-layout/index.tsx +25 -96
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +79 -118
- package/src/client/components/layout/desktop-layout/resizer/separator-drag.ts +140 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +1 -1
- package/src/client/components/layout/session-sidebar/Header.tsx +1 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +27 -13
- package/src/client/components/layout/session-sidebar/SessionList.tsx +1 -1
- package/src/client/components/layout/session-sidebar/SubagentList.tsx +6 -4
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +1 -1
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +1 -1
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +2 -2
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +14 -4
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +3 -0
- package/src/client/components/workspace/chat-timeline/QueueList.tsx +18 -14
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +3 -1
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +23 -3
- package/src/client/components/workspace/chat-timeline/index.tsx +7 -2
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Goal.tsx +3 -3
- package/src/client/components/workspace/git-panel/BranchToolbar.tsx +2 -2
- package/src/client/components/workspace/git-panel/index.tsx +5 -1
- package/src/client/components/workspace/model-dropdown/groups.ts +35 -2
- package/src/client/components/workspace/model-dropdown/index.tsx +48 -7
- package/src/client/components/workspace/model-dropdown/use-catalog.ts +28 -4
- package/src/client/components/workspace/terminal-panel/Header.tsx +51 -19
- package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +86 -282
- package/src/client/components/workspace/terminal-panel/index.tsx +76 -50
- package/src/client/components/workspace/terminal-panel/mount.ts +275 -0
- package/src/client/data/theme/terminal.ts +27 -29
- package/src/client/hooks/chat/composer/index.ts +28 -8
- package/src/client/hooks/chat/omp/session-list.tsx +19 -2
- package/src/client/hooks/chat/omp/session-sidebar-controller.ts +1 -1
- package/src/client/hooks/chat/timeline/actions.ts +47 -28
- package/src/client/hooks/chat/timeline/deferred-model.test.ts +219 -0
- package/src/client/hooks/chat/timeline/deferred-model.ts +92 -0
- package/src/client/hooks/chat/timeline/index.ts +25 -21
- package/src/client/hooks/chat/timeline/queue.ts +110 -46
- package/src/client/hooks/chat/timeline/send.ts +16 -5
- package/src/client/hooks/workspace/available-width.ts +33 -0
- package/src/client/hooks/workspace/panel-widths.ts +34 -37
- package/src/client/hooks/workspace/terminal/index.ts +217 -0
- package/src/client/hooks/workspace/terminal/socket.ts +227 -0
- package/src/server/db.server.ts +19 -0
- package/src/server/lib/fs/git-sync.ts +191 -0
- package/src/server/lib/models/preferences.server.ts +110 -0
- package/src/server/lib/models/registry.server.ts +206 -0
- package/src/server/lib/omp/rpc/manager.ts +58 -3
- package/src/server/lib/omp/rpc/session-commands.ts +6 -0
- package/src/server/lib/omp/rpc/utility.ts +7 -1
- package/src/server/lib/omp/session/sidebar-data.server.ts +3 -3
- package/src/server/lib/queue/delivery.server.test.ts +203 -0
- package/src/server/lib/queue/delivery.server.ts +144 -0
- package/src/server/lib/queue/schema-migration.server.ts +50 -0
- package/src/server/lib/queue/store.server.ts +185 -0
- package/src/server/lib/terminal/foreground.ts +73 -0
- package/src/server/lib/terminal/history.test.ts +118 -0
- package/src/server/lib/terminal/history.ts +205 -0
- package/src/server/lib/terminal/runtime.server.ts +338 -0
- package/src/server/lib/terminal/session.server.ts +215 -0
- package/src/server/lib/terminal/shell.test.ts +97 -0
- package/src/server/lib/terminal/shell.ts +103 -0
- package/src/server/routes/fs/git.ts +22 -12
- package/src/server/routes/index.ts +2 -1
- package/src/server/routes/models/root.ts +88 -215
- package/src/server/routes/sessions/index.ts +11 -3
- package/src/server/routes/sessions/queue.ts +121 -0
- package/src/server/routes/sessions/session.ts +1 -132
- package/src/server/routes/settings/commands.ts +59 -18
- package/src/server/routes/terminal/index.ts +2 -2
- package/src/server/routes/terminal/run.ts +8 -100
- package/src/server/routes/terminal/sessions.ts +24 -0
- package/src/server/routes/terminal/ws.ts +149 -0
- package/src/shared/lib/chat/composer/client.ts +17 -8
- package/src/shared/lib/chat/composer/commands.ts +290 -0
- package/src/shared/lib/chat/composer/composer.test.ts +157 -0
- package/src/shared/lib/chat/composer/filter.ts +21 -7
- package/src/shared/lib/chat/composer/trigger.ts +109 -20
- package/src/shared/lib/chat/omp/agent-events.test.ts +37 -1
- package/src/shared/lib/chat/omp/agent-events.ts +30 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +9 -1
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +45 -8
- package/src/shared/lib/models/preferences.test.ts +131 -0
- package/src/shared/lib/models/preferences.ts +88 -0
- package/src/shared/lib/models/provider-models.ts +8 -1
- package/src/shared/lib/omp/session/stream-state.server.ts +2 -3
- package/src/shared/lib/omp/subagent/label.test.ts +94 -0
- package/src/shared/lib/omp/subagent/label.ts +84 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +113 -18
- package/src/shared/lib/workspace/panel-widths.ts +161 -30
- package/src/shared/lib/workspace/right-panels.ts +50 -11
- package/src/shared/lib/workspace/session-state/store.ts +4 -4
- package/src/shared/lib/workspace/terminal/protocol.ts +222 -0
- package/src/shared/types/chat.ts +38 -65
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/models.ts +101 -0
- package/src/shared/types/omp/agent.ts +12 -0
- package/src/shared/types/omp/subagent.ts +4 -0
- package/src/shared/types/settings/command.ts +13 -0
- package/src/shared/types/terminal.ts +17 -15
- package/src/shared/types/workspace.ts +2 -2
- package/dist/client/static/css/index.5c0447cefe.css +0 -2
- package/dist/client/static/js/async/g.d1c2fe09aa.js +0 -2
- package/dist/client/static/js/async/x4.69ec69c61b.js +0 -6
- package/dist/client/static/js/index.dc5f5fabe8.js +0 -33
- package/src/client/components/workspace/terminal-panel/InputBar.tsx +0 -82
- package/src/client/components/workspace/terminal-panel/LogEntry.tsx +0 -70
- package/src/client/hooks/workspace/terminal-output.ts +0 -66
- package/src/client/hooks/workspace/terminal.ts +0 -283
- package/src/server/lib/fs/terminal-cwd.ts +0 -49
- package/src/server/routes/terminal/stream.ts +0 -100
- /package/dist/client/static/js/async/{5b.0d6ae50fe8.js.LICENSE.txt → 5b.a5ccbc19dc.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.d1c2fe09aa.js.LICENSE.txt → g.a06ca3b3e9.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{m.0896dab823.js.LICENSE.txt → m.c038c71b9f.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.dc5f5fabe8.js.LICENSE.txt → index.6e065017eb.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { runShell, shellOk } from '@/server/lib/fs/shell';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ahead/behind counts behind the git panel's Sync button.
|
|
5
|
+
*
|
|
6
|
+
* `git rev-list --left-right --count HEAD...@{upstream}` counts against the
|
|
7
|
+
* LOCAL remote-tracking ref, and that ref only moves on fetch/pull/push.
|
|
8
|
+
* Nothing in the chamber ever fetched, so `behind` reported the state of the
|
|
9
|
+
* last fetch rather than the remote: the badge sat at ↑0 ↓0 while origin had
|
|
10
|
+
* commits to pull, and the button — disabled by that very zero — could never
|
|
11
|
+
* correct itself.
|
|
12
|
+
*
|
|
13
|
+
* A read that asks for counts therefore refreshes the tracking ref first. The
|
|
14
|
+
* refresh is TTL'd so the git panel's 5s poll is not a 5s fetch loop, it is
|
|
15
|
+
* shared across the panels polling the same repo, and the loader starts it
|
|
16
|
+
* before its local git probes so the round trip overlaps instead of adding up.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** How long a fetch attempt — success or failure — is trusted. */
|
|
20
|
+
const FETCH_TTL_MS = 60_000;
|
|
21
|
+
/**
|
|
22
|
+
* How long a read waits for the fetch before answering from the tracking ref
|
|
23
|
+
* as it stands. A slow or offline remote must not stall the panel; the fetch
|
|
24
|
+
* keeps running under its own budget and a later read picks up the new ref.
|
|
25
|
+
*/
|
|
26
|
+
const FETCH_WAIT_MS = 3_000;
|
|
27
|
+
/** Hard budget for one fetch; it is killed at the end of it. */
|
|
28
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
29
|
+
|
|
30
|
+
export interface GitSyncCount {
|
|
31
|
+
ahead: number;
|
|
32
|
+
behind: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RemoteState {
|
|
36
|
+
/** When the last attempt settled; `0` means never attempted. */
|
|
37
|
+
at: number;
|
|
38
|
+
/** The running attempt, so concurrent polls share one fetch. */
|
|
39
|
+
inFlight: Promise<void> | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const remoteStates = new Map<string, RemoteState>();
|
|
43
|
+
|
|
44
|
+
function stateFor(cwd: string): RemoteState {
|
|
45
|
+
let state = remoteStates.get(cwd);
|
|
46
|
+
if (!state) {
|
|
47
|
+
state = { at: 0, inFlight: null };
|
|
48
|
+
remoteStates.set(cwd, state);
|
|
49
|
+
}
|
|
50
|
+
return state;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Runs one `git fetch` with a hard deadline, returning whether it succeeded.
|
|
55
|
+
*
|
|
56
|
+
* Deliberately not `runShell`: that helper reads stdout and stderr to EOF, and
|
|
57
|
+
* a stalled fetch defeats it — git's own `git-remote-https` child survives the
|
|
58
|
+
* kill and keeps the inherited stderr pipe open, so the read never ends and the
|
|
59
|
+
* promise hangs forever. Verified on Bun 1.4.2: Bun's `timeout` closes stdout
|
|
60
|
+
* but not that inherited stderr, and the orphan lingers. Nothing here reads the
|
|
61
|
+
* fetch's output, so both streams are ignored (no pipe, nothing to hang on) and
|
|
62
|
+
* the process is killed by process group at the deadline — `detached` is what
|
|
63
|
+
* makes the group killable, and the group kill is what stops the helper from
|
|
64
|
+
* accumulating on every stalled attempt.
|
|
65
|
+
*/
|
|
66
|
+
async function runFetch(cwd: string, remote: string): Promise<boolean> {
|
|
67
|
+
const proc = Bun.spawn({
|
|
68
|
+
cmd: ['sh', '-c', `git fetch --quiet --no-tags '${remote.replace(/'/g, `'\\''`)}'`],
|
|
69
|
+
cwd,
|
|
70
|
+
stdin: 'ignore',
|
|
71
|
+
stdout: 'ignore',
|
|
72
|
+
stderr: 'ignore',
|
|
73
|
+
detached: true,
|
|
74
|
+
// Without this a remote that wants credentials blocks on stdin; the fetch
|
|
75
|
+
// then fails instead, and the counts fall back to the tracking ref.
|
|
76
|
+
env: { ...Bun.env, GIT_TERMINAL_PROMPT: '0' },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
80
|
+
const deadline = new Promise<void>((resolve) => {
|
|
81
|
+
timer = setTimeout(resolve, FETCH_TIMEOUT_MS);
|
|
82
|
+
});
|
|
83
|
+
const exitCode = await Promise.race([
|
|
84
|
+
proc.exited,
|
|
85
|
+
deadline.then(() => {
|
|
86
|
+
try {
|
|
87
|
+
process.kill(-proc.pid, 'SIGKILL');
|
|
88
|
+
} catch {
|
|
89
|
+
// Already gone, or the group could not be signalled: `exited` below
|
|
90
|
+
// still settles, and the ref simply stays as it was.
|
|
91
|
+
}
|
|
92
|
+
return proc.exited;
|
|
93
|
+
}),
|
|
94
|
+
]);
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
return exitCode === 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The branch's upstream remote, or null when it has none. Cheap and local, so
|
|
101
|
+
* the no-upstream case is never cached: configuring an upstream in the terminal
|
|
102
|
+
* shows up on the next poll instead of up to a TTL later.
|
|
103
|
+
*/
|
|
104
|
+
async function readUpstreamRemote(cwd: string): Promise<string | null> {
|
|
105
|
+
try {
|
|
106
|
+
const result = await runShell('git rev-parse --abbrev-ref --symbolic-full-name @{upstream}', {
|
|
107
|
+
cwd,
|
|
108
|
+
timeout: 5_000,
|
|
109
|
+
});
|
|
110
|
+
if (!shellOk(result)) return null;
|
|
111
|
+
const upstream = result.stdout.trim();
|
|
112
|
+
const slash = upstream.indexOf('/');
|
|
113
|
+
return slash > 0 ? upstream.slice(0, slash) : null;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Refresh `cwd`'s remote-tracking refs, awaiting at most `FETCH_WAIT_MS`.
|
|
121
|
+
* Resolves immediately when the last attempt is still inside its TTL, and
|
|
122
|
+
* shares the running attempt when one is already in flight.
|
|
123
|
+
*/
|
|
124
|
+
export async function refreshRemoteRefs(cwd: string): Promise<void> {
|
|
125
|
+
const state = stateFor(cwd);
|
|
126
|
+
if (state.inFlight) return state.inFlight;
|
|
127
|
+
if (state.at !== 0 && Date.now() - state.at < FETCH_TTL_MS) return;
|
|
128
|
+
|
|
129
|
+
const remote = await readUpstreamRemote(cwd);
|
|
130
|
+
if (!remote) return;
|
|
131
|
+
// Reading the upstream is itself an await: another poll may have started the
|
|
132
|
+
// fetch, or completed one, in the meantime.
|
|
133
|
+
if (state.inFlight) return state.inFlight;
|
|
134
|
+
if (state.at !== 0 && Date.now() - state.at < FETCH_TTL_MS) return;
|
|
135
|
+
|
|
136
|
+
// The attempt never rejects — a failed fetch is not a broken panel — and
|
|
137
|
+
// always clears `inFlight` so a later read can retry.
|
|
138
|
+
state.inFlight = (async () => {
|
|
139
|
+
try {
|
|
140
|
+
await runFetch(cwd, remote);
|
|
141
|
+
} finally {
|
|
142
|
+
state.at = Date.now();
|
|
143
|
+
state.inFlight = null;
|
|
144
|
+
}
|
|
145
|
+
})();
|
|
146
|
+
|
|
147
|
+
// Bounded wait. `AbortSignal.timeout` rather than `Bun.sleep`: it is unref'd,
|
|
148
|
+
// so an abandoned wait cannot hold a live timer open on every poll that
|
|
149
|
+
// refreshes.
|
|
150
|
+
const deadline = new Promise<void>((resolve) => {
|
|
151
|
+
AbortSignal.timeout(FETCH_WAIT_MS).addEventListener('abort', () => resolve(), { once: true });
|
|
152
|
+
});
|
|
153
|
+
await Promise.race([state.inFlight, deadline]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Marks `cwd` as freshly fetched — a pull/push has already moved the tracking
|
|
158
|
+
* ref, so the next read must not fetch a state it just produced.
|
|
159
|
+
*/
|
|
160
|
+
export function markRemoteRefsFresh(cwd: string): void {
|
|
161
|
+
stateFor(cwd).at = Date.now();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Forces the next read to fetch. The counts are per-branch, so a checkout makes
|
|
166
|
+
* whatever the previous branch refreshed irrelevant.
|
|
167
|
+
*/
|
|
168
|
+
export function invalidateRemoteRefs(cwd: string): void {
|
|
169
|
+
stateFor(cwd).at = 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Ahead/behind against the branch's upstream, read from the local tracking ref
|
|
174
|
+
* (`refreshRemoteRefs` is what makes that ref current). Both zero when the
|
|
175
|
+
* branch has no upstream or git cannot answer.
|
|
176
|
+
*/
|
|
177
|
+
export async function gitSyncCount(cwd: string): Promise<GitSyncCount> {
|
|
178
|
+
try {
|
|
179
|
+
// `git rev-list --left-right --count HEAD...@{upstream}` prints "<ahead>\t<behind>".
|
|
180
|
+
const result = await runShell('git rev-list --left-right --count HEAD...@{upstream}', {
|
|
181
|
+
cwd,
|
|
182
|
+
timeout: 8_000,
|
|
183
|
+
});
|
|
184
|
+
if (!shellOk(result)) return { ahead: 0, behind: 0 };
|
|
185
|
+
|
|
186
|
+
const [ahead, behind] = result.stdout.trim().split(/\s+/).map(Number);
|
|
187
|
+
return { ahead: ahead || 0, behind: behind || 0 };
|
|
188
|
+
} catch {
|
|
189
|
+
return { ahead: 0, behind: 0 };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Persistence for the user's favorite + last-used models.
|
|
8
|
+
*
|
|
9
|
+
* Lives apart from the model registry because a favorite is NOT a property of
|
|
10
|
+
* omp's registry: the catalog the route used to mutate (`omp_models_catalog`)
|
|
11
|
+
* is a MOCK-only demo list that real mode never reads back, so a star clicked
|
|
12
|
+
* on a real install was written into a dead row and lost on the next render.
|
|
13
|
+
* This store is the single source for both modes.
|
|
14
|
+
*
|
|
15
|
+
* Keys are composite (`provider:id`) — see `modelKey` — and stale ones are
|
|
16
|
+
* dropped on read, so a model removed from models.yml cannot hold a slot in the
|
|
17
|
+
* five-entry recent rail forever.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { getDb } from '@/server/db.server';
|
|
21
|
+
import { readSettingsJson, writeSettingsJson } from '@/server/lib/db/settings-store';
|
|
22
|
+
import { isMockMode } from '@/server/mock.server';
|
|
23
|
+
import { INITIAL_MODELS_CATALOG } from '@/client/data/models/catalog';
|
|
24
|
+
import {
|
|
25
|
+
EMPTY_MODEL_PREFERENCES,
|
|
26
|
+
RECENT_MODELS_LIMIT,
|
|
27
|
+
filterKnownKeys,
|
|
28
|
+
readModelPreferences,
|
|
29
|
+
recordRecentKey,
|
|
30
|
+
toggleFavoriteKey,
|
|
31
|
+
} from '@/shared/lib/models/preferences';
|
|
32
|
+
import { modelKey } from '@/shared/lib/models/identity';
|
|
33
|
+
import type { ModelPreferences } from '@/shared/types';
|
|
34
|
+
|
|
35
|
+
const MODEL_PREFERENCES_KEY = 'omp_model_preferences';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Demo favorites/recents so a `MOCK=true` chamber still opens with the rails
|
|
39
|
+
* the shipped catalog advertises. Seeded on first read only — from then on the
|
|
40
|
+
* row is the user's, and the catalog's own flags are ignored.
|
|
41
|
+
*/
|
|
42
|
+
function mockSeedPreferences(): ModelPreferences {
|
|
43
|
+
return {
|
|
44
|
+
favorites: INITIAL_MODELS_CATALOG.filter((model) => model.isFavorite).map(modelKey),
|
|
45
|
+
recentKeys: INITIAL_MODELS_CATALOG.filter((model) => model.isRecent).map(modelKey).slice(0, RECENT_MODELS_LIMIT),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readStoredPreferences(): Promise<ModelPreferences> {
|
|
50
|
+
const db = await getDb();
|
|
51
|
+
const raw = await readSettingsJson<unknown>(db, MODEL_PREFERENCES_KEY, null);
|
|
52
|
+
if (raw === null) return isMockMode() ? mockSeedPreferences() : EMPTY_MODEL_PREFERENCES;
|
|
53
|
+
return readModelPreferences(raw);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Preferences narrowed to the models the registry actually serves right now —
|
|
58
|
+
* the shape every loader response carries.
|
|
59
|
+
*/
|
|
60
|
+
export async function readModelPreferencesFor(
|
|
61
|
+
models: readonly { provider: string; id: string }[],
|
|
62
|
+
): Promise<ModelPreferences> {
|
|
63
|
+
try {
|
|
64
|
+
const known = new Set(models.map(modelKey));
|
|
65
|
+
const stored = await readStoredPreferences();
|
|
66
|
+
return {
|
|
67
|
+
favorites: filterKnownKeys(stored.favorites, known),
|
|
68
|
+
recentKeys: filterKnownKeys(stored.recentKeys, known),
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
return EMPTY_MODEL_PREFERENCES;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Persist a mutation with the stale keys already removed, so a key the registry
|
|
77
|
+
* dropped cannot sit in the row holding one of the five recent slots. Reading
|
|
78
|
+
* prunes for display only — without this the slot stayed occupied across
|
|
79
|
+
* reloads and the RECENT rail silently showed four models instead of five.
|
|
80
|
+
*/
|
|
81
|
+
async function writePreferences(known: ReadonlySet<string>, mutate: (current: ModelPreferences) => ModelPreferences): Promise<ModelPreferences> {
|
|
82
|
+
const current = await readStoredPreferences();
|
|
83
|
+
const next = mutate({
|
|
84
|
+
favorites: filterKnownKeys(current.favorites, known),
|
|
85
|
+
recentKeys: filterKnownKeys(current.recentKeys, known),
|
|
86
|
+
});
|
|
87
|
+
const db = await getDb();
|
|
88
|
+
await writeSettingsJson(db, MODEL_PREFERENCES_KEY, next);
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Flip one model's favorite flag, returning the full persisted pair. */
|
|
93
|
+
export async function toggleFavoritePreference(key: string, known: ReadonlySet<string>): Promise<ModelPreferences> {
|
|
94
|
+
return writePreferences(known, (current) => ({
|
|
95
|
+
...current,
|
|
96
|
+
favorites: toggleFavoriteKey(current.favorites, key),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Record a model as last used. Called from the selection action rather than
|
|
102
|
+
* from a separate endpoint: "the user picked this model" is one event, and a
|
|
103
|
+
* second request would only add a way for the two to disagree.
|
|
104
|
+
*/
|
|
105
|
+
export async function recordRecentPreference(key: string, known: ReadonlySet<string>): Promise<ModelPreferences> {
|
|
106
|
+
return writePreferences(known, (current) => ({
|
|
107
|
+
...current,
|
|
108
|
+
recentKeys: recordRecentKey(current.recentKeys, key),
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Assembly of the chat model registry that `GET /api/models` serves: the omp
|
|
8
|
+
* registry (auth providers + models.yml) narrowed by the provider/model hides
|
|
9
|
+
* the operator set, plus the live session's default model.
|
|
10
|
+
*
|
|
11
|
+
* Extracted from `routes/models/root.ts` so the route keeps only its loader and
|
|
12
|
+
* action, and so the preference store can share the same model list.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getDb } from '@/server/db.server';
|
|
16
|
+
import { PROVIDERS_SETTINGS_KEY } from '@/server/lib/models/provider-registry.server';
|
|
17
|
+
import { readSettingsJson } from '@/server/lib/db/settings-store';
|
|
18
|
+
import { readDisabledProviders } from '@/server/lib/omp/config/roles';
|
|
19
|
+
import { runUtilityCommand, type OmpModel } from '@/server/lib/omp/rpc/utility';
|
|
20
|
+
import { invalidateModelsCaches } from '@/shared/lib/models/server-cache';
|
|
21
|
+
import type { ModelsData, ProviderItem } from '@/shared/types';
|
|
22
|
+
|
|
23
|
+
const MODELS_CACHE_TTL_MS = 60_000;
|
|
24
|
+
export const SAFE_MODEL_LOAD_FAILURE_MESSAGE = 'Model list is temporarily unavailable. Check your configuration and try again.';
|
|
25
|
+
|
|
26
|
+
const modelNameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
|
27
|
+
|
|
28
|
+
function compareModelEntries(a: { id: string; name: string; provider: string }, b: { id: string; name: string; provider: string }): number {
|
|
29
|
+
return modelNameCollator.compare(a.name || a.id, b.name || b.id)
|
|
30
|
+
|| modelNameCollator.compare(a.provider, b.provider)
|
|
31
|
+
|| modelNameCollator.compare(a.id, b.id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// "off" is always a valid selector; the concrete efforts come from the model's
|
|
35
|
+
// baked thinking metadata (omp: getSupportedEfforts = reasoning ? efforts : []).
|
|
36
|
+
function thinkingLevelsFor(model: OmpModel): string[] {
|
|
37
|
+
if (!model.reasoning) return ['off'];
|
|
38
|
+
return ['off', ...(model.thinking?.efforts ?? [])];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function supportsFastMode(model: OmpModel): boolean {
|
|
42
|
+
return model.provider === 'anthropic' || model.provider === 'openai' || model.provider === 'google';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Disabled providers recorded in the chamber's own provider overlay. Used only
|
|
47
|
+
* by the MOCK path of `/api/models`, which has no config.yml `disabledProviders`
|
|
48
|
+
* to read — real mode gets the same answer natively.
|
|
49
|
+
*/
|
|
50
|
+
function isDisabledStoredProvider(value: unknown): value is ProviderItem {
|
|
51
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
52
|
+
const candidate = value as ProviderItem;
|
|
53
|
+
return typeof candidate.slug === 'string' && candidate.disabled === true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function readStoredProvidersForModels(): Promise<ProviderItem[]> {
|
|
57
|
+
try {
|
|
58
|
+
const db = await getDb();
|
|
59
|
+
const parsed = await readSettingsJson<unknown>(db, PROVIDERS_SETTINGS_KEY, null);
|
|
60
|
+
if (!Array.isArray(parsed)) return [];
|
|
61
|
+
return parsed.filter(isDisabledStoredProvider);
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hiddenModelKey(provider: string, modelId: string): string {
|
|
68
|
+
return `${provider.trim().toLowerCase()}:${modelId}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readHiddenModelKeys(): Promise<Set<string>> {
|
|
72
|
+
try {
|
|
73
|
+
const db = await getDb();
|
|
74
|
+
const parsed = await readSettingsJson<unknown>(db, PROVIDERS_SETTINGS_KEY, null);
|
|
75
|
+
if (!Array.isArray(parsed)) return new Set();
|
|
76
|
+
const hidden = new Set<string>();
|
|
77
|
+
for (const provider of parsed) {
|
|
78
|
+
if (typeof provider !== 'object' || provider === null) continue;
|
|
79
|
+
const { slug, models } = provider as { slug?: unknown; models?: unknown };
|
|
80
|
+
if (typeof slug !== 'string' || !Array.isArray(models)) continue;
|
|
81
|
+
for (const model of models) {
|
|
82
|
+
if (typeof model !== 'object' || model === null) continue;
|
|
83
|
+
const { id, isVisible } = model as { id?: unknown; isVisible?: unknown };
|
|
84
|
+
if (typeof id === 'string' && isVisible === false) hidden.add(hiddenModelKey(slug, id));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return hidden;
|
|
88
|
+
} catch {
|
|
89
|
+
return new Set();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A model is offered only when its provider is neither disabled in omp's
|
|
95
|
+
* config.yml (`disabledProviders`) nor carrying a per-model hide in the
|
|
96
|
+
* chamber overlay. Disabling the provider is the coarse switch — Settings →
|
|
97
|
+
* Providers flips it so the whole provider leaves the chat picker.
|
|
98
|
+
*/
|
|
99
|
+
function filterSelectableModels(
|
|
100
|
+
available: OmpModel[],
|
|
101
|
+
disabledProviders: Set<string>,
|
|
102
|
+
hiddenModelKeys: Set<string>,
|
|
103
|
+
): OmpModel[] {
|
|
104
|
+
return available.filter((model) => (
|
|
105
|
+
!disabledProviders.has(model.provider)
|
|
106
|
+
&& !hiddenModelKeys.has(hiddenModelKey(model.provider, model.id))
|
|
107
|
+
));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isOmpModelEntry(value: unknown): value is OmpModel {
|
|
111
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
112
|
+
const candidate = value as OmpModel;
|
|
113
|
+
return typeof candidate.id === 'string' && typeof candidate.provider === 'string';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isLoginProvider(value: unknown): value is { id: string; name: string; authenticated: boolean } {
|
|
117
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
118
|
+
const candidate = value as { id?: unknown; name?: unknown; authenticated?: unknown };
|
|
119
|
+
return typeof candidate.id === 'string'
|
|
120
|
+
&& typeof candidate.name === 'string'
|
|
121
|
+
&& typeof candidate.authenticated === 'boolean';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function loadModels(): Promise<ModelsData> {
|
|
125
|
+
const availableResponse = await runUtilityCommand<{ models?: unknown }>(
|
|
126
|
+
{ type: 'get_available_models' },
|
|
127
|
+
120_000,
|
|
128
|
+
);
|
|
129
|
+
const available = Array.isArray(availableResponse.models)
|
|
130
|
+
? availableResponse.models
|
|
131
|
+
.filter(isOmpModelEntry)
|
|
132
|
+
.map((model) => ({
|
|
133
|
+
...model,
|
|
134
|
+
name: typeof model.name === 'string' && model.name.trim().length > 0 ? model.name : model.id,
|
|
135
|
+
}))
|
|
136
|
+
: [];
|
|
137
|
+
|
|
138
|
+
const disabledProviders = await readDisabledProviders();
|
|
139
|
+
const hiddenModelKeys = await readHiddenModelKeys();
|
|
140
|
+
const nameMap: Record<string, string> = {};
|
|
141
|
+
const thinkingLevels: Record<string, string[]> = {};
|
|
142
|
+
const modelList = filterSelectableModels(available, disabledProviders, hiddenModelKeys)
|
|
143
|
+
.map((m) => ({
|
|
144
|
+
id: m.id,
|
|
145
|
+
name: m.name,
|
|
146
|
+
provider: m.provider,
|
|
147
|
+
thinkingLevels: thinkingLevelsFor(m),
|
|
148
|
+
supportsFastMode: supportsFastMode(m),
|
|
149
|
+
contextWindow: m.contextWindow ?? undefined,
|
|
150
|
+
maxTokens: m.maxTokens ?? undefined,
|
|
151
|
+
cost: m.cost,
|
|
152
|
+
}))
|
|
153
|
+
.sort(compareModelEntries);
|
|
154
|
+
|
|
155
|
+
const loginResponse = await runUtilityCommand<{ providers?: unknown }>(
|
|
156
|
+
{ type: 'get_login_providers' },
|
|
157
|
+
30_000,
|
|
158
|
+
);
|
|
159
|
+
const loginProviders = Array.isArray(loginResponse.providers)
|
|
160
|
+
? loginResponse.providers.filter(isLoginProvider)
|
|
161
|
+
: [];
|
|
162
|
+
const connectedProviders = loginProviders
|
|
163
|
+
.filter((provider) => provider.authenticated)
|
|
164
|
+
.map((provider) => ({ id: provider.id, name: provider.name, disabled: disabledProviders.has(provider.id) }));
|
|
165
|
+
for (const m of available) {
|
|
166
|
+
const key = `${m.provider}:${m.id}`;
|
|
167
|
+
nameMap[key] = m.name;
|
|
168
|
+
thinkingLevels[key] = thinkingLevelsFor(m);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let defaultModel: { provider: string; modelId: string } | null = null;
|
|
172
|
+
try {
|
|
173
|
+
const state = await runUtilityCommand<{ model?: { provider?: string; id?: string } }>(
|
|
174
|
+
{ type: 'get_state' },
|
|
175
|
+
30_000,
|
|
176
|
+
);
|
|
177
|
+
const provider = state.model?.provider;
|
|
178
|
+
const modelId = state.model?.id;
|
|
179
|
+
if (provider && modelId && available.some((m) => m.provider === provider && m.id === modelId)) {
|
|
180
|
+
defaultModel = { provider, modelId };
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Default model is cosmetic — the models list is still useful without it.
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { models: nameMap, modelList, defaultModel, thinkingLevels, connectedProviders };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const EMPTY_MODELS: ModelsData = {
|
|
190
|
+
models: {},
|
|
191
|
+
modelList: [],
|
|
192
|
+
defaultModel: null,
|
|
193
|
+
thinkingLevels: {},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export async function loadModelsWithCache(): Promise<ModelsData> {
|
|
197
|
+
const cached = globalThis.__ompChamberModelsCache;
|
|
198
|
+
if (cached && cached.expiresAt > Date.now()) return cached.data;
|
|
199
|
+
const data = await loadModels();
|
|
200
|
+
globalThis.__ompChamberModelsCache = { data, expiresAt: Date.now() + MODELS_CACHE_TTL_MS };
|
|
201
|
+
return data;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function invalidateModelsCache(): void {
|
|
205
|
+
invalidateModelsCaches();
|
|
206
|
+
}
|
|
@@ -20,8 +20,9 @@ import { PendingUiDialogs } from '@/server/lib/omp/rpc/pending-ui-dialogs';
|
|
|
20
20
|
import { clearSessionFileCaches } from '@/server/lib/omp/session/files';
|
|
21
21
|
import { notifyRunningChange } from '@/server/lib/omp/rpc/session-registry';
|
|
22
22
|
import { dispatchSessionCommand } from '@/server/lib/omp/rpc/session-commands';
|
|
23
|
+
import { scheduleQueueDelivery } from '@/server/lib/queue/delivery.server';
|
|
23
24
|
import { SubagentLiveness } from '@/server/lib/omp/rpc/subagent-liveness';
|
|
24
|
-
import { markStreamStatus } from '@/shared/lib/omp/session/stream-state.server';
|
|
25
|
+
import { loadStreamStatuses, markStreamStatus } from '@/shared/lib/omp/session/stream-state.server';
|
|
25
26
|
import { GET_STATE_TIMEOUT_MS, IDLE_DESTROY_MS, NON_TERMINAL_CONTINUATION_GRACE_MS, READY_TIMEOUT_MS, SUBAGENT_STALE_MS, type AgentEvent, type EventListener, type RpcSessionState } from '@/server/lib/omp/rpc/constants';
|
|
26
27
|
|
|
27
28
|
export type {
|
|
@@ -172,6 +173,22 @@ export class AgentSessionWrapper {
|
|
|
172
173
|
this.destroy();
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
/** Write the terminal stream badge from the ending turn's own stopReason.
|
|
177
|
+
* The `abort` command already wrote `abort` at dispatch time, but the
|
|
178
|
+
* agent_end frame arrives later and previously flattened it to `finish`.
|
|
179
|
+
* Re-check the current row and only upgrade `stream` rows, so a `finish`
|
|
180
|
+
* written here can never clobber a newer run's live `stream`/`abort`. */
|
|
181
|
+
private async markEndStatus(messages: unknown): Promise<void> {
|
|
182
|
+
if (!this.sessionId) return;
|
|
183
|
+
const aborted = Array.isArray(messages) && messages.some((entry) => {
|
|
184
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
|
|
185
|
+
return (entry as Record<string, unknown>).stopReason === 'aborted';
|
|
186
|
+
});
|
|
187
|
+
const current = await loadStreamStatuses();
|
|
188
|
+
if (current[this.sessionId] !== 'stream') return;
|
|
189
|
+
await markStreamStatus(this.sessionId, aborted ? 'abort' : 'finish');
|
|
190
|
+
}
|
|
191
|
+
|
|
175
192
|
private handleFrame(frame: RpcFrame): void {
|
|
176
193
|
this.resetIdleTimer();
|
|
177
194
|
const event = frame as AgentEvent;
|
|
@@ -188,6 +205,31 @@ export class AgentSessionWrapper {
|
|
|
188
205
|
refreshSessionList = true;
|
|
189
206
|
if (this.sessionId) void markStreamStatus(this.sessionId, 'stream');
|
|
190
207
|
break;
|
|
208
|
+
case 'turn_start':
|
|
209
|
+
// Redundant with agent_start in the happy path, but the run-level
|
|
210
|
+
// counterpart can be missed (e.g. the state was cleared by a command
|
|
211
|
+
// between frames). The turn is proof enough the session is live.
|
|
212
|
+
if (this.sessionId) void markStreamStatus(this.sessionId, 'stream');
|
|
213
|
+
break;
|
|
214
|
+
case 'message_start':
|
|
215
|
+
// Same recovery as turn_start: a message frame is only emitted inside
|
|
216
|
+
// a live run, so its arrival re-arms the `stream` row no matter what
|
|
217
|
+
// stale status sits there (upsert overwrites any terminal badge).
|
|
218
|
+
if (this.sessionId) void markStreamStatus(this.sessionId, 'stream');
|
|
219
|
+
break;
|
|
220
|
+
case 'turn_end': {
|
|
221
|
+
// A multi-turn run emits turn_end for EVERY turn — intermediates end
|
|
222
|
+
// with `toolUse`/`stop` while the run keeps going (verified against
|
|
223
|
+
// omp 18.2.8: agent_start → turn_start → turn_end('toolUse') →
|
|
224
|
+
// turn_start → turn_end('stop') → agent_end, and the frame carries no
|
|
225
|
+
// isTerminal field). Only `aborted` is unambiguous here: the run is
|
|
226
|
+
// over, so the abort badge can be written before agent_end arrives.
|
|
227
|
+
// Everything else waits for agent_end, which knows isTerminal.
|
|
228
|
+
if (!this.sessionId) break;
|
|
229
|
+
const turn = event.message as Record<string, unknown> | undefined;
|
|
230
|
+
if (turn?.stopReason === 'aborted') void markStreamStatus(this.sessionId, 'abort');
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
191
233
|
case 'agent_end':
|
|
192
234
|
if (event.isTerminal !== false) {
|
|
193
235
|
this.streaming = false;
|
|
@@ -196,7 +238,21 @@ export class AgentSessionWrapper {
|
|
|
196
238
|
this.awaitingAgentStartDeadline = 0;
|
|
197
239
|
this.continuationGraceUntil = 0;
|
|
198
240
|
clearSessionFileCaches();
|
|
199
|
-
|
|
241
|
+
// The turn's own stopReason is the ground truth for how it ended:
|
|
242
|
+
// `aborted` = user stop, anything else (`stop`, `toolUse`, and the
|
|
243
|
+
// ambiguous `error` that tool failures also produce) completes as
|
|
244
|
+
// `finish`. This must not overwrite a fresh `abort` row from a NEWER
|
|
245
|
+
// run — guard against downgrades by checking the current row first.
|
|
246
|
+
const aborted = Array.isArray(event.messages) && event.messages.some((entry) => {
|
|
247
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
|
|
248
|
+
return (entry as Record<string, unknown>).stopReason === 'aborted';
|
|
249
|
+
});
|
|
250
|
+
if (this.sessionId) void this.markEndStatus(event.messages);
|
|
251
|
+
// The run truly ended — the server, not the browser, decides whether
|
|
252
|
+
// a queued follow-up goes out next. A user-aborted run holds the
|
|
253
|
+
// queue (stop-all semantics): the next run end or an explicit send
|
|
254
|
+
// picks it up.
|
|
255
|
+
if (!aborted) scheduleQueueDelivery(this);
|
|
200
256
|
} else {
|
|
201
257
|
this.continuationGraceUntil = Date.now() + NON_TERMINAL_CONTINUATION_GRACE_MS;
|
|
202
258
|
}
|
|
@@ -232,7 +288,6 @@ export class AgentSessionWrapper {
|
|
|
232
288
|
this.promptRunning = false;
|
|
233
289
|
this.awaitingAgentStart = false;
|
|
234
290
|
this.awaitingAgentStartDeadline = 0;
|
|
235
|
-
if (this.sessionId) void markStreamStatus(this.sessionId, 'error');
|
|
236
291
|
this.emit({ type: 'prompt_error', errorMessage: (event.error as string) ?? 'Prompt failed' });
|
|
237
292
|
notifyRunningChange();
|
|
238
293
|
return;
|
|
@@ -13,6 +13,7 @@ import { RpcCommandTimeoutError, type RpcProcess } from '@/server/lib/omp/rpc/pr
|
|
|
13
13
|
import { AWAITING_AGENT_START_TIMEOUT_MS, GET_STATE_TIMEOUT_MS, IMAGE_BEARING_COMMANDS, PASSTHROUGH_COMMANDS, PROMPT_ACK_TIMEOUT_MS, RESTARTING_MESSAGE, SESSION_BUSY_MESSAGE, WebRpcError, toImageContents, type AgentEvent, type RpcSessionState, validateAgentImages } from '@/server/lib/omp/rpc/constants';
|
|
14
14
|
import { clearSessionFileCaches } from '@/server/lib/omp/session/files';
|
|
15
15
|
import { notifyRunningChange } from '@/server/lib/omp/rpc/session-registry';
|
|
16
|
+
import { scheduleQueueDelivery } from '@/server/lib/queue/delivery.server';
|
|
16
17
|
import { clearStreamStatus, markStreamStatus } from '@/shared/lib/omp/session/stream-state.server';
|
|
17
18
|
import { buildWebState, type WebStateHost } from '@/server/lib/omp/rpc/web-state';
|
|
18
19
|
|
|
@@ -28,6 +29,8 @@ export interface SessionCommandHost extends WebStateHost {
|
|
|
28
29
|
/** Real omp session id (empty before the first get_state). */
|
|
29
30
|
sessionId: string;
|
|
30
31
|
emit(event: AgentEvent): void;
|
|
32
|
+
/** One dispatch throat (same surface the wrapper's own send uses). */
|
|
33
|
+
send(command: Record<string, unknown>): Promise<unknown>;
|
|
31
34
|
resetIdleTimer(force?: boolean): void;
|
|
32
35
|
/** Forget a pending ask/approval dialog once its response is sent. */
|
|
33
36
|
resolvePendingUiDialog(id: string): void;
|
|
@@ -100,6 +103,9 @@ export async function dispatchSessionCommand(host: SessionCommandHost, command:
|
|
|
100
103
|
releaseStreamRow();
|
|
101
104
|
host.emit({ type: 'prompt_result', agentInvoked: false });
|
|
102
105
|
notifyRunningChange();
|
|
106
|
+
// Nothing ran (agent was idle and declined) — the queue may hold the
|
|
107
|
+
// next item; give it the same delivery window a run end would.
|
|
108
|
+
scheduleQueueDelivery(host);
|
|
103
109
|
} else if (!streamingBehavior && ack?.agentInvoked !== false) {
|
|
104
110
|
host.awaitingAgentStart = true;
|
|
105
111
|
host.awaitingAgentStartDeadline = Date.now() + AWAITING_AGENT_START_TIMEOUT_MS;
|
|
@@ -22,7 +22,13 @@ import { RpcProcess } from '@/server/lib/omp/rpc/process';
|
|
|
22
22
|
|
|
23
23
|
// Extensions stay ENABLED: they can register models and login providers, and
|
|
24
24
|
// omitting them made the web UI's model/provider lists disagree with the CLI's.
|
|
25
|
-
|
|
25
|
+
//
|
|
26
|
+
// Skills stay ENABLED too. `--no-skills` made `get_available_commands` omit
|
|
27
|
+
// every `skill:<name>` entry (measured: 45 commands without, 52 with), so the
|
|
28
|
+
// composer's `/` popup could not offer the skills omp happily executes — and
|
|
29
|
+
// the chamber's own disk scan missed provider roots omp reads (`~/.agents/skills`).
|
|
30
|
+
// Cost is nil: the ready frame lands at the same ~420ms either way.
|
|
31
|
+
const UTILITY_EXTRA_ARGS = ['--no-session', '--no-lsp'];
|
|
26
32
|
const READY_TIMEOUT_MS = 60_000;
|
|
27
33
|
// Longer than the 60s models-cache TTL on purpose: with idle-kill == TTL every
|
|
28
34
|
// pause past a minute paid a cold multi-second respawn on top of the stale
|
|
@@ -110,11 +110,11 @@ export async function loadSidebarData(): Promise<SessionListPayload> {
|
|
|
110
110
|
const sidebarSort = await serverSidebarSort();
|
|
111
111
|
|
|
112
112
|
// Live stream status per session (spinner / one-shot done badge), written by
|
|
113
|
-
// the RPC manager on agent_start/agent_end
|
|
114
|
-
// session is no longer running are stale (restart mid-run) and heal to
|
|
113
|
+
// the RPC manager on agent_start/agent_end and by the abort command. `stream`
|
|
114
|
+
// rows whose session is no longer running are stale (restart mid-run) and heal to
|
|
115
115
|
// `finish` right here — the authoritative status travels with the same
|
|
116
116
|
// fetch that refreshes the sidebar list.
|
|
117
|
-
const streamStatuses: Record<string, 'stream' | 'finish' | 'abort'
|
|
117
|
+
const streamStatuses: Record<string, 'stream' | 'finish' | 'abort'> = {};
|
|
118
118
|
// Sessions blocked on a dialog nobody has answered yet. Read live from the
|
|
119
119
|
// process registry (never persisted): the child that owns the question is the
|
|
120
120
|
// same thing that owns the flag, so a restart cannot leave a stale badge.
|