ompchamber 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -6
- package/dist/client/index.html +1 -1
- package/dist/client/static/js/async/dc.6dc4b0a04b.js +2 -0
- package/dist/client/static/js/{index.bf1b1fe988.js → index.0ac93ee878.js} +3 -3
- package/package.json +1 -2
- package/src/cli/lib/args.js +1 -0
- package/src/cli/lib/commands/serve.js +7 -0
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +8 -2
- package/src/client/components/workspace/file-explorer/index.tsx +6 -0
- package/src/client/data/models/catalog.ts +8 -0
- package/src/client/data/samples/dialogue-session.ts +1 -1
- package/src/client/data/samples/sample-tools/other.ts +1 -1
- package/src/client/hooks/chat/timeline/send.ts +1 -1
- package/src/server/index.ts +53 -0
- package/src/server/lib/models/provider-registry.server.ts +44 -20
- package/src/server/lib/omp/core/cli.ts +3 -2
- package/src/server/lib/omp/core/startup.ts +64 -0
- package/src/server/lib/omp/rpc/process.ts +1 -1
- package/src/server/mock.server.ts +6 -12
- package/src/server/routes/chat/stream.ts +13 -30
- package/src/server/routes/models/root.ts +5 -1
- package/src/shared/lib/chat/stream-service.ts +0 -76
- package/src/shared/lib/fs/paths.ts +19 -0
- package/dist/client/static/js/async/art.62139ed479.js +0 -2
- /package/dist/client/static/js/async/{art.62139ed479.js.LICENSE.txt → dc.6dc4b0a04b.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.bf1b1fe988.js.LICENSE.txt → index.0ac93ee878.js.LICENSE.txt} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ompchamber",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Developer web console and diagnostic chamber for AI Oh-My-Pi — Elysia + Preact on Bun.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@fontsource/fira-code": "^5.3.0",
|
|
48
|
-
"@google/genai": "^2.21.0",
|
|
49
48
|
"@shikijs/langs": "^4.4.3",
|
|
50
49
|
"@shikijs/themes": "^4.4.3",
|
|
51
50
|
"@tailwindcss/typography": "^0.5.20",
|
package/src/cli/lib/args.js
CHANGED
|
@@ -216,6 +216,7 @@ ENVIRONMENT:
|
|
|
216
216
|
OMPCHAMBER_DATA_DIR Override the data directory (default: ~/.ompchamber)
|
|
217
217
|
OMPCHAMBER_PORT Default web server port
|
|
218
218
|
OMPCHAMBER_HOST Default bind address
|
|
219
|
+
OMPCHAMBER_OMP_BIN Path to the omp binary (required; falls back to PATH)
|
|
219
220
|
GITHUB_TOKEN Raise the GitHub API rate limit for update checks
|
|
220
221
|
|
|
221
222
|
EXAMPLES:
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '@/cli/lib/runtime.js';
|
|
11
11
|
import { log, ok, warn, fail, printJson, isJson, isQuiet } from '@/cli/lib/output.js';
|
|
12
12
|
import { wireChildProcessLifecycle } from '@/cli/lib/process-lifecycle.js';
|
|
13
|
+
import { ompStartupError } from '@/server/lib/omp/core/startup';
|
|
13
14
|
|
|
14
15
|
const DEFAULT_PORT = 3000;
|
|
15
16
|
const DEFAULT_HOST = '127.0.0.1';
|
|
@@ -45,6 +46,12 @@ export async function run(options, ctx) {
|
|
|
45
46
|
const quiet = isQuiet();
|
|
46
47
|
const pkgRoot = ctx?.pkgRoot ?? process.cwd();
|
|
47
48
|
|
|
49
|
+
// The same gate the server entry applies, run here first so a missing omp
|
|
50
|
+
// binary fails immediately — with no detached child spawned, no log file to
|
|
51
|
+
// read and no registry entry left pointing at a process that already exited.
|
|
52
|
+
const ompError = ompStartupError();
|
|
53
|
+
if (ompError) fail(ompError);
|
|
54
|
+
|
|
48
55
|
if (mode === 'prod') {
|
|
49
56
|
const serverEntry = joinPath(pkgRoot, 'dist', 'client', 'index.html');
|
|
50
57
|
// Bun.file().exists() is file-only (false for directories) — correct here,
|
|
@@ -5,12 +5,16 @@ import { useFetcher } from '@/client/lib/router/fetcher';
|
|
|
5
5
|
import { FileIcon } from '@/client/components/common/file-icon';
|
|
6
6
|
import { FileContextMenu, FileDeleteModal, FileHistoryModal, FileRenameModal } from '@/client/components/workspace/file-explorer/Modals';
|
|
7
7
|
import { getGitStatusInfo, type FolderGitStatusInfo } from '@/shared/lib/fs/git-status';
|
|
8
|
+
import { toAbsolutePath } from '@/shared/lib/fs/paths';
|
|
9
|
+
import { copyToClipboard } from '@/client/hooks/ui/clipboard';
|
|
8
10
|
import type { GitChange } from '@/shared/types/git';
|
|
9
11
|
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
10
12
|
|
|
11
13
|
interface FileTreeItemProps {
|
|
12
14
|
file: any;
|
|
13
15
|
rootPath?: string;
|
|
16
|
+
/** Absolute base dir of the listing (server-reported `root`), anchors Copy Path. */
|
|
17
|
+
basePath?: string;
|
|
14
18
|
repo?: string;
|
|
15
19
|
onLoadChildren?: (path: string) => Promise<void>;
|
|
16
20
|
onOpenFile?: (file: any) => void;
|
|
@@ -24,6 +28,7 @@ interface FileTreeItemProps {
|
|
|
24
28
|
export function FileTreeItem({
|
|
25
29
|
file,
|
|
26
30
|
rootPath,
|
|
31
|
+
basePath,
|
|
27
32
|
repo,
|
|
28
33
|
onLoadChildren,
|
|
29
34
|
onOpenFile,
|
|
@@ -151,9 +156,9 @@ export function FileTreeItem({
|
|
|
151
156
|
fd.append('path', file.path);
|
|
152
157
|
submitAction(fd);
|
|
153
158
|
} else if (actionType === 'copy_path') {
|
|
154
|
-
|
|
159
|
+
void copyToClipboard(toAbsolutePath(basePath, file.path));
|
|
155
160
|
} else if (actionType === 'copy_relative') {
|
|
156
|
-
|
|
161
|
+
void copyToClipboard(file.path);
|
|
157
162
|
} else if (actionType === 'history') {
|
|
158
163
|
setShowHistoryModal(true);
|
|
159
164
|
const fd = new FormData();
|
|
@@ -236,6 +241,7 @@ export function FileTreeItem({
|
|
|
236
241
|
key={child.id}
|
|
237
242
|
file={child}
|
|
238
243
|
rootPath={rootPath}
|
|
244
|
+
basePath={basePath}
|
|
239
245
|
repo={repo}
|
|
240
246
|
onLoadChildren={onLoadChildren}
|
|
241
247
|
onOpenFile={onOpenFile}
|
|
@@ -10,6 +10,10 @@ import { usePanelRefresh, useFileMutationRefresh } from '@/client/hooks/workspac
|
|
|
10
10
|
|
|
11
11
|
export function FileExplorer({ className = '', enabled = true, rootPath, onOpenFile, refreshKey = 0, onRefresh }: { className?: string, enabled?: boolean, rootPath?: string, onOpenFile?: (file: any) => void, refreshKey?: number, onRefresh?: () => void }) {
|
|
12
12
|
const [tree, setTree] = useState<any[]>([]);
|
|
13
|
+
// Absolute base dir reported by `/api/fs/dir` for the current listing — the
|
|
14
|
+
// workspace root, or the selected nested repo. Copy Path anchors on it
|
|
15
|
+
// because the client's `rootPath` may be rejected/unset.
|
|
16
|
+
const [listingRoot, setListingRoot] = useState('');
|
|
13
17
|
const [searchQuery, setSearchQuery] = useSessionState<string>('files.searchQuery', '');
|
|
14
18
|
const [isLoading, setIsLoading] = useState(false);
|
|
15
19
|
const [storedExpandedPaths, setStoredExpandedPaths, expandedPathsReady] = useSessionState<string[]>('files.expandedPaths', []);
|
|
@@ -43,6 +47,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
43
47
|
fetch(listUrl())
|
|
44
48
|
.then(r => r.json())
|
|
45
49
|
.then(data => {
|
|
50
|
+
if (typeof data.root === 'string') setListingRoot(data.root);
|
|
46
51
|
if (Array.isArray(data.files)) {
|
|
47
52
|
setTree(rehydrateTree(data.files, childrenCacheRef.current, expandedPaths));
|
|
48
53
|
}
|
|
@@ -176,6 +181,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
176
181
|
key={file.id}
|
|
177
182
|
file={file}
|
|
178
183
|
rootPath={rootPath}
|
|
184
|
+
basePath={listingRoot}
|
|
179
185
|
repo={activeRepo}
|
|
180
186
|
onLoadChildren={loadChildren}
|
|
181
187
|
onOpenFile={onOpenFile}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { AIModelOption } from '@/shared/types';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* MOCK-only demo catalog: the model rows a `MOCK=true` chamber shows before the
|
|
5
|
+
* operator adds any of their own.
|
|
6
|
+
*
|
|
7
|
+
* Real mode never reads this — `/api/models` there is fed by the live omp model
|
|
8
|
+
* registry, and its mutations start from an empty list — so a demo model can
|
|
9
|
+
* never leak into a real installation (see `routes/models/root.ts`).
|
|
10
|
+
*/
|
|
3
11
|
export const INITIAL_MODELS_CATALOG: AIModelOption[] = [
|
|
4
12
|
// FAVORITES (As seen in image.png)
|
|
5
13
|
{
|
|
@@ -277,7 +277,7 @@ export function getSampleDialogueSession(): {
|
|
|
277
277
|
id: '65e71033',
|
|
278
278
|
role: 'ai',
|
|
279
279
|
content:
|
|
280
|
-
'Repo summary from `RepoSummarizer`:\n\n**OMPChamber** — developer web-view console and diagnostic chamber for AI **Oh-My-Pi** autonomous agent.\n\n**Stack:** Remix v2 + React 19 + Vite, Tailwind CSS v4, TypeScript (strict, ES2022). Runs under npm or Bun.\n\n**Structure:**\n- `app/routes/` — Remix flat-routes, large REST-style `api/` domain tree\n- `app/components/` — common, layout, workspace, mobile, settings\n- `app/hooks/` — chat, terminal, git-tree, agent streaming\n- `app/lib/omp/` — bridges to oh-my-pi runtime (RPC managers, session readers)\n- `app/types/`, `app/data/` (mock presets)\n\n**Config:** `MOCK` mode toggles seeded demo data vs real data. SQLite persistence (`app/db.server.ts`).\n\n**Deployment-relevant:** PWA support,
|
|
280
|
+
'Repo summary from `RepoSummarizer`:\n\n**OMPChamber** — developer web-view console and diagnostic chamber for AI **Oh-My-Pi** autonomous agent.\n\n**Stack:** Remix v2 + React 19 + Vite, Tailwind CSS v4, TypeScript (strict, ES2022). Runs under npm or Bun.\n\n**Structure:**\n- `app/routes/` — Remix flat-routes, large REST-style `api/` domain tree\n- `app/components/` — common, layout, workspace, mobile, settings\n- `app/hooks/` — chat, terminal, git-tree, agent streaming\n- `app/lib/omp/` — bridges to oh-my-pi runtime (RPC managers, session readers)\n- `app/types/`, `app/data/` (mock presets)\n\n**Config:** `MOCK` mode toggles seeded demo data vs real data. SQLite persistence (`app/db.server.ts`).\n\n**Deployment-relevant:** PWA support, xterm/KaTeX/Shiki rendering.',
|
|
281
281
|
date: 'Sep 9, 04:44 AM',
|
|
282
282
|
},
|
|
283
283
|
],
|
|
@@ -88,7 +88,7 @@ export const otherTools: ToolCallData[] = [
|
|
|
88
88
|
target: 'app/mock.server.ts',
|
|
89
89
|
output: `1: export function isMockMode(): boolean {
|
|
90
90
|
2: const envVal = (Bun.env.MOCK || "").trim().toLowerCase();
|
|
91
|
-
3: return envVal
|
|
91
|
+
3: return envVal === "true" || envVal === "1" || envVal === "on" || envVal === "yes";
|
|
92
92
|
4: }`,
|
|
93
93
|
status: 'success',
|
|
94
94
|
duration: '14ms',
|
|
@@ -311,7 +311,7 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
|
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
// Mock / chamber-created session: existing
|
|
314
|
+
// Mock / chamber-created session: existing simulated SSE path.
|
|
315
315
|
// Cancel any previous stream
|
|
316
316
|
optimisticUserIdRef.current = null;
|
|
317
317
|
if (abortControllerRef.current) {
|
package/src/server/index.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { Elysia } from 'elysia';
|
|
2
2
|
import { apiRoutes } from '@/server/routes';
|
|
3
3
|
import { ssrRoutes } from '@/server/plugins/ssr';
|
|
4
|
+
import { getDatabasePath } from '@/server/db.server';
|
|
5
|
+
import { fetchOmpRegistrySnapshot } from '@/server/lib/models/provider-registry.server';
|
|
6
|
+
import { ompStartupError, ompStartupLogLines } from '@/server/lib/omp/core/startup';
|
|
7
|
+
import { isMockMode } from '@/server/mock.server';
|
|
8
|
+
|
|
9
|
+
// Refuse to run without a resolvable omp binary — before the listener opens and
|
|
10
|
+
// before the first request can reach a route that shells out to it. The database
|
|
11
|
+
// is opened lazily by getDb(), so this exits without touching it.
|
|
12
|
+
const startupError = ompStartupError();
|
|
13
|
+
if (startupError) {
|
|
14
|
+
console.error(startupError);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Which omp the chamber drives, which ~/.omp tree it reads and which SQLite file
|
|
19
|
+
// it writes — the paths every session/agent diagnostic traces back to. The data
|
|
20
|
+
// mode comes first because it changes what every later line means.
|
|
21
|
+
console.log(`[ompchamber] mock mode: ${isMockMode() ? 'true (demo presets)' : 'false (real omp data)'}`);
|
|
22
|
+
for (const line of ompStartupLogLines()) console.log(`[ompchamber] ${line}`);
|
|
23
|
+
console.log(`[ompchamber] db: ${await getDatabasePath()}`);
|
|
4
24
|
|
|
5
25
|
const port = Number(Bun.env.PORT) || 3000;
|
|
6
26
|
const host = Bun.env.HOST || 'localhost';
|
|
@@ -9,8 +29,41 @@ const app = new Elysia().use(apiRoutes).use(ssrRoutes);
|
|
|
9
29
|
|
|
10
30
|
await app.listen({ port, hostname: host });
|
|
11
31
|
|
|
32
|
+
// The listener is already accepting requests here, so the probe only postpones
|
|
33
|
+
// this last banner line, never startup. Printing it after the probe keeps the
|
|
34
|
+
// one line that matters — the URL — at the bottom of the log.
|
|
35
|
+
await logDetectedRegistry();
|
|
36
|
+
|
|
12
37
|
console.log(`[ompchamber] listening on http://${host}:${port}`);
|
|
13
38
|
|
|
39
|
+
/**
|
|
40
|
+
* What omp reports it can actually run. A live RPC round-trip: a cold utility
|
|
41
|
+
* process takes seconds, which is why it runs after the listener is up. Side
|
|
42
|
+
* benefit — it warms the shared utility process the model picker drives, so the
|
|
43
|
+
* first /api/models call does not pay the spawn.
|
|
44
|
+
*/
|
|
45
|
+
async function logDetectedRegistry(): Promise<void> {
|
|
46
|
+
if (isMockMode()) {
|
|
47
|
+
console.log('[ompchamber] providers: n/a (MOCK mode)');
|
|
48
|
+
console.log('[ompchamber] models: n/a (MOCK mode)');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const { providers, models } = await fetchOmpRegistrySnapshot();
|
|
53
|
+
// Count providers that actually serve a model — the distinct `provider`
|
|
54
|
+
// values in the model list, i.e. the same set GET /api/models groups by.
|
|
55
|
+
// omp's login catalog lists every sign-in offer it supports (75 here, 73 of
|
|
56
|
+
// them without credentials); counting those would report a provider set the
|
|
57
|
+
// chamber cannot use. The catalog size is kept as context.
|
|
58
|
+
const servingProviders = new Set(models.map((model) => model.provider)).size;
|
|
59
|
+
console.log(`[ompchamber] providers: ${servingProviders} detected (${providers.length} known to omp)`);
|
|
60
|
+
console.log(`[ompchamber] models: ${models.length} detected`);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
console.log(`[ompchamber] providers/models: detection failed — ${message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
14
67
|
for (const signal of ['SIGTERM', 'SIGINT'] as const) {
|
|
15
68
|
process.once(signal, () => {
|
|
16
69
|
void app.stop();
|
|
@@ -23,12 +23,54 @@ export const PROVIDERS_SETTINGS_KEY = 'omp_providers_config';
|
|
|
23
23
|
|
|
24
24
|
const RPC_CACHE_TTL_MS = 60_000;
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
/** Minimal shape of a get_login_providers entry, as the chamber reads it. */
|
|
27
|
+
export interface DetectedLoginProvider {
|
|
27
28
|
id: string;
|
|
28
29
|
name: string;
|
|
29
30
|
authenticated: boolean;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function isDetectedLoginProvider(provider: unknown): provider is DetectedLoginProvider {
|
|
34
|
+
return (
|
|
35
|
+
typeof provider === 'object' && provider !== null
|
|
36
|
+
&& typeof (provider as { id?: unknown }).id === 'string'
|
|
37
|
+
&& typeof (provider as { name?: unknown }).name === 'string'
|
|
38
|
+
&& typeof (provider as { authenticated?: unknown }).authenticated === 'boolean'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isOmpModel(model: unknown): model is OmpModel {
|
|
43
|
+
return (
|
|
44
|
+
typeof model === 'object' && model !== null
|
|
45
|
+
&& typeof (model as OmpModel).id === 'string'
|
|
46
|
+
&& typeof (model as OmpModel).provider === 'string'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface OmpRegistrySnapshot {
|
|
51
|
+
/** Login providers omp reports (authenticated or not). */
|
|
52
|
+
providers: DetectedLoginProvider[];
|
|
53
|
+
/** Every model omp currently serves. */
|
|
54
|
+
models: OmpModel[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Raw omp registry probe — the two commands that define "detected" for the
|
|
59
|
+
* chamber: the login providers omp knows and the models they serve. Parsed here
|
|
60
|
+
* once so the provider settings page and the startup banner count the same set.
|
|
61
|
+
* Throws when the utility RPC is unreachable; each caller degrades its own way.
|
|
62
|
+
*/
|
|
63
|
+
export async function fetchOmpRegistrySnapshot(): Promise<OmpRegistrySnapshot> {
|
|
64
|
+
const [loginResponse, modelsResponse] = await Promise.all([
|
|
65
|
+
runUtilityCommand<{ providers?: unknown }>({ type: 'get_login_providers' }, 30_000),
|
|
66
|
+
runUtilityCommand<{ models?: unknown }>({ type: 'get_available_models' }, 60_000),
|
|
67
|
+
]);
|
|
68
|
+
return {
|
|
69
|
+
providers: Array.isArray(loginResponse.providers) ? loginResponse.providers.filter(isDetectedLoginProvider) : [],
|
|
70
|
+
models: Array.isArray(modelsResponse.models) ? modelsResponse.models.filter(isOmpModel) : [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
32
74
|
/**
|
|
33
75
|
* Provider registry straight from the omp agent: authenticated login providers
|
|
34
76
|
* (omp auth) plus their registered models (get_available_models). This is the
|
|
@@ -38,25 +80,7 @@ async function loadRpcProviderItems(): Promise<ProviderItem[]> {
|
|
|
38
80
|
const cached = globalThis.__ompChamberProvidersRpcCache;
|
|
39
81
|
if (cached && cached.expiresAt > Date.now()) return cached.data;
|
|
40
82
|
try {
|
|
41
|
-
const
|
|
42
|
-
runUtilityCommand<{ providers?: unknown }>({ type: 'get_login_providers' }, 30_000),
|
|
43
|
-
runUtilityCommand<{ models?: unknown }>({ type: 'get_available_models' }, 60_000),
|
|
44
|
-
]);
|
|
45
|
-
const loginProviders = Array.isArray(loginResponse.providers)
|
|
46
|
-
? loginResponse.providers.filter((provider): provider is LoginProvider => (
|
|
47
|
-
typeof provider === 'object' && provider !== null
|
|
48
|
-
&& typeof (provider as { id?: unknown }).id === 'string'
|
|
49
|
-
&& typeof (provider as { name?: unknown }).name === 'string'
|
|
50
|
-
&& typeof (provider as { authenticated?: unknown }).authenticated === 'boolean'
|
|
51
|
-
))
|
|
52
|
-
: [];
|
|
53
|
-
const available = Array.isArray(modelsResponse.models)
|
|
54
|
-
? modelsResponse.models.filter((model): model is OmpModel => (
|
|
55
|
-
typeof model === 'object' && model !== null
|
|
56
|
-
&& typeof (model as OmpModel).id === 'string'
|
|
57
|
-
&& typeof (model as OmpModel).provider === 'string'
|
|
58
|
-
))
|
|
59
|
-
: [];
|
|
83
|
+
const { providers: loginProviders, models: available } = await fetchOmpRegistrySnapshot();
|
|
60
84
|
const disabled = await readDisabledProviders().catch(() => new Set<string>());
|
|
61
85
|
|
|
62
86
|
const items = loginProviders.map((provider) => {
|
|
@@ -20,6 +20,7 @@ let cachedBin: string | null = null;
|
|
|
20
20
|
let binMissAt = 0;
|
|
21
21
|
|
|
22
22
|
const BIN_NAME = process.platform === 'win32' ? 'omp.exe' : 'omp';
|
|
23
|
+
|
|
23
24
|
// Only successes are cached for the process lifetime. omp may be installed (or
|
|
24
25
|
// PATH repaired) while the server runs; a permanently cached "not found" would
|
|
25
26
|
// keep the UI reporting a missing binary until restart.
|
|
@@ -32,7 +33,7 @@ export function invalidateOmpCliCache(): void {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function probeOmpBin(): string | null {
|
|
35
|
-
const override = Bun.env.
|
|
36
|
+
const override = Bun.env.OMPCHAMBER_OMP_BIN;
|
|
36
37
|
if (override) return existsSync(override) ? override : null;
|
|
37
38
|
|
|
38
39
|
const onPath = Bun.which(BIN_NAME);
|
|
@@ -49,7 +50,7 @@ function probeOmpBin(): string | null {
|
|
|
49
50
|
return Bun.which(BIN_NAME, { PATH: fallbackPath });
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
/** Resolve the omp binary:
|
|
53
|
+
/** Resolve the omp binary: OMPCHAMBER_OMP_BIN override, then PATH lookup. Returns
|
|
53
54
|
* null when omp is not installed. A hit is cached for the process lifetime; a
|
|
54
55
|
* miss is re-probed after MISS_TTL_MS. */
|
|
55
56
|
export function resolveOmpBin(): string | null {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Startup gate: the chamber must not run without a resolvable `omp` binary.
|
|
8
|
+
*
|
|
9
|
+
* Every live capability — RPC agent sessions, config get/set, session state,
|
|
10
|
+
* model lists, updates — shells out to that binary (see ./cli.ts), so a missing
|
|
11
|
+
* install is not a degraded mode to browse into: the UI would open onto an
|
|
12
|
+
* empty shell whose every action fails. Both entry points call this before
|
|
13
|
+
* binding a port (src/server/index.ts) or spawning a detached server
|
|
14
|
+
* (src/cli/lib/commands/serve.js), turning the failure into one actionable line
|
|
15
|
+
* instead of a health check that never passes.
|
|
16
|
+
*
|
|
17
|
+
* `MOCK=true` is exempt by definition — demo mode exists to run on presets
|
|
18
|
+
* without a real omp install.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { resolveOmpBin } from '@/server/lib/omp/core/cli';
|
|
22
|
+
import { getAgentDir, getConfigRoot } from '@/server/lib/omp/core/paths';
|
|
23
|
+
import { isMockMode } from '@/server/mock.server';
|
|
24
|
+
|
|
25
|
+
/** Null when the app may start; otherwise the message to print before exiting. */
|
|
26
|
+
export function ompStartupError(): string | null {
|
|
27
|
+
if (isMockMode()) return null;
|
|
28
|
+
if (resolveOmpBin()) return null;
|
|
29
|
+
return [
|
|
30
|
+
'omp binary not found — OMPChamber cannot start.',
|
|
31
|
+
'Agent sessions, omp config, session state and updates all run through the `omp` CLI,',
|
|
32
|
+
'so the binary must be resolvable before the server starts.',
|
|
33
|
+
'Install AI Oh-My-Pi and put `omp` on your PATH, or set OMPCHAMBER_OMP_BIN=/path/to/omp.',
|
|
34
|
+
'To run on demo data without an omp install, start with MOCK=true.',
|
|
35
|
+
].join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface OmpStartupPaths {
|
|
39
|
+
/** Resolved `omp` executable, or null when unresolved (mock mode). */
|
|
40
|
+
bin: string | null;
|
|
41
|
+
/** `~/.omp` — PI_CONFIG_DIR rename applied. */
|
|
42
|
+
configDir: string;
|
|
43
|
+
/** `~/.omp/agent` — PI_CODING_AGENT_DIR override applied. */
|
|
44
|
+
agentDir: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Startup banner lines. The caller owns the prefix.
|
|
48
|
+
*
|
|
49
|
+
* These are the locations the running process actually uses: the executable it
|
|
50
|
+
* spawns and the agent tree it reads. Printing them makes a misdirected install
|
|
51
|
+
* (two omps on PATH, a stray PI_CODING_AGENT_DIR) visible in the log instead of
|
|
52
|
+
* inferred from empty session lists. */
|
|
53
|
+
export function ompStartupLogLines(): string[] {
|
|
54
|
+
const paths: OmpStartupPaths = {
|
|
55
|
+
bin: resolveOmpBin(),
|
|
56
|
+
configDir: getConfigRoot(),
|
|
57
|
+
agentDir: getAgentDir(),
|
|
58
|
+
};
|
|
59
|
+
return [
|
|
60
|
+
`omp binary: ${paths.bin ?? 'not found (MOCK mode — presets only)'}`,
|
|
61
|
+
`omp config dir: ${paths.configDir}`,
|
|
62
|
+
`omp agent dir: ${paths.agentDir}`,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
@@ -46,7 +46,7 @@ export class RpcProcess {
|
|
|
46
46
|
const resolveBin = options.dependencies?.resolveOmpBin ?? resolveOmpBin;
|
|
47
47
|
const bin = resolveBin();
|
|
48
48
|
if (!bin) {
|
|
49
|
-
throw new Error('omp binary not found. Install oh-my-pi or set
|
|
49
|
+
throw new Error('omp binary not found. Install oh-my-pi or set OMPCHAMBER_OMP_BIN.');
|
|
50
50
|
}
|
|
51
51
|
this.cwd = options.cwd;
|
|
52
52
|
if (options.onFrame) this.frameListeners.add(options.onFrame);
|
|
@@ -5,29 +5,23 @@
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Utility to determine whether the application is running in MOCK mode.
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* - When MOCK=true (or MOCK=1): all data uses mock / simulated presets (demo chat traces, mock token analytics, mock settings presets).
|
|
10
10
|
* - When MOCK=false (or MOCK=0): all data uses real data (real SQLite persistence, real token calculations, real workspace state).
|
|
11
|
+
* - Unset (or any unrecognized value) defaults to REAL data — the chamber is a
|
|
12
|
+
* diagnostic console for a live omp install, so demo presets must be opted
|
|
13
|
+
* into explicitly rather than inherited by omission.
|
|
11
14
|
*/
|
|
12
15
|
export function isMockMode(): boolean {
|
|
13
16
|
const envVal = (Bun.env.MOCK || '').trim().toLowerCase();
|
|
14
|
-
|
|
15
|
-
if (envVal === 'false' || envVal === '0' || envVal === 'off' || envVal === 'no') {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (envVal === 'true' || envVal === '1' || envVal === 'on' || envVal === 'yes') {
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
return true;
|
|
18
|
+
return envVal === 'true' || envVal === '1' || envVal === 'on' || envVal === 'yes';
|
|
25
19
|
}
|
|
26
20
|
|
|
27
21
|
export function getMockModeInfo(): { isMock: boolean; rawEnv: string } {
|
|
28
22
|
const isMock = isMockMode();
|
|
29
23
|
return {
|
|
30
24
|
isMock,
|
|
31
|
-
rawEnv: Bun.env.MOCK || (isMock ? 'true
|
|
25
|
+
rawEnv: Bun.env.MOCK || (isMock ? 'true' : 'false (default)'),
|
|
32
26
|
};
|
|
33
27
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@/server/lib/remix-compat';
|
|
2
2
|
import { isMockMode } from '@/server/mock.server';
|
|
3
|
-
import {
|
|
3
|
+
import { handleSimulatedStreaming } from '@/shared/lib/chat/stream-service';
|
|
4
4
|
import { createSseStream } from '@/server/lib/sse';
|
|
5
5
|
|
|
6
6
|
export async function loader({ request }: LoaderFunctionArgs) {
|
|
@@ -31,7 +31,7 @@ async function handleStreamingRequest(request: Request) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Real mode routes through the omp agent RPC bridge (POST /api/agent/:id +
|
|
34
|
-
// SSE /api/agent/:id/events). This
|
|
34
|
+
// SSE /api/agent/:id/events). This simulated stream is the MOCK-only
|
|
35
35
|
// path — refuse it in real mode so the two never conflict.
|
|
36
36
|
if (!isMockMode()) {
|
|
37
37
|
return new Response(JSON.stringify({ error: 'Streaming is handled by the omp agent bridge in real mode' }), {
|
|
@@ -68,34 +68,17 @@ async function handleStreamingRequest(request: Request) {
|
|
|
68
68
|
model: modelName,
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
timeStr,
|
|
83
|
-
sendEvent,
|
|
84
|
-
isAborted: () => isAborted,
|
|
85
|
-
});
|
|
86
|
-
} else {
|
|
87
|
-
// Fallback / simulated intelligent streaming for OMPChamber
|
|
88
|
-
await handleSimulatedStreaming({
|
|
89
|
-
prompt,
|
|
90
|
-
modelName,
|
|
91
|
-
workspaceName,
|
|
92
|
-
sessionId,
|
|
93
|
-
messageId,
|
|
94
|
-
timeStr,
|
|
95
|
-
sendEvent,
|
|
96
|
-
isAborted: () => isAborted,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
71
|
+
// Simulated intelligent streaming — the MOCK-only path for OMPChamber.
|
|
72
|
+
await handleSimulatedStreaming({
|
|
73
|
+
prompt,
|
|
74
|
+
modelName,
|
|
75
|
+
workspaceName,
|
|
76
|
+
sessionId,
|
|
77
|
+
messageId,
|
|
78
|
+
timeStr,
|
|
79
|
+
sendEvent,
|
|
80
|
+
isAborted: () => isAborted,
|
|
81
|
+
});
|
|
99
82
|
} catch (err: any) {
|
|
100
83
|
if (!isAborted) {
|
|
101
84
|
sendEvent('error', { error: err?.message || 'Streaming execution error' });
|
|
@@ -274,7 +274,11 @@ export async function action({ request }: ActionFunctionArgs) {
|
|
|
274
274
|
const body = await request.json();
|
|
275
275
|
|
|
276
276
|
const catalog = await readSettingsJson<unknown>(db, MODELS_CATALOG_KEY, null);
|
|
277
|
-
|
|
277
|
+
// The shipped demo catalog is MOCK-only: a real install starts from an empty
|
|
278
|
+
// list, so a demo model can never be persisted into its stored catalog.
|
|
279
|
+
let models: AIModelOption[] = Array.isArray(catalog)
|
|
280
|
+
? catalog
|
|
281
|
+
: (isMockMode() ? INITIAL_MODELS_CATALOG : []);
|
|
278
282
|
|
|
279
283
|
const { actionType, modelId, provider, thinkingLevel, model, models: newModels } = body;
|
|
280
284
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getDb } from '@/server/db.server';
|
|
2
|
-
import { GoogleGenAI } from '@google/genai';
|
|
3
2
|
import type { ChatMessageData, ToolCallData } from '@/shared/types';
|
|
4
3
|
|
|
5
4
|
// Helper to delay execution for realistic stream simulation
|
|
@@ -35,81 +34,6 @@ async function persistAiMessage(sessionId: string, aiMessage: ChatMessageData) {
|
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
// Handler for real Gemini API streaming
|
|
39
|
-
export async function handleGeminiStreaming(options: {
|
|
40
|
-
apiKey: string;
|
|
41
|
-
prompt: string;
|
|
42
|
-
modelName: string;
|
|
43
|
-
sessionId: string;
|
|
44
|
-
messageId: string;
|
|
45
|
-
timeStr: string;
|
|
46
|
-
sendEvent: StreamSendEvent;
|
|
47
|
-
isAborted: () => boolean;
|
|
48
|
-
}) {
|
|
49
|
-
const { apiKey, prompt, sessionId, messageId, timeStr, sendEvent, isAborted } = options;
|
|
50
|
-
const ai = new GoogleGenAI({
|
|
51
|
-
apiKey,
|
|
52
|
-
httpOptions: {
|
|
53
|
-
headers: {
|
|
54
|
-
'User-Agent': 'aistudio-build',
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
sendEvent('thinking_start', { title: 'Deep Reasoning' });
|
|
60
|
-
const thoughtText = `Deconstructing prompt: "${prompt}".\nAnalyzing project context, evaluating edge dependencies, and generating validated response.`;
|
|
61
|
-
|
|
62
|
-
// Stream thinking chunk
|
|
63
|
-
sendEvent('thinking_chunk', { delta: thoughtText });
|
|
64
|
-
sendEvent('thinking_end', {
|
|
65
|
-
thought: thoughtText,
|
|
66
|
-
summary: 'Analyze context and stream response',
|
|
67
|
-
duration: '0.8s',
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
sendEvent('content_start', {});
|
|
71
|
-
|
|
72
|
-
const responseStream = await ai.models.generateContentStream({
|
|
73
|
-
model: 'gemini-3.8-flash',
|
|
74
|
-
contents: prompt || 'Hello',
|
|
75
|
-
config: {
|
|
76
|
-
systemInstruction: 'You are OMPChamber AI Assistant, integrated with Oh-My-Pi, remisJS, and Bun runtime environments. Provide concise, clean, markdown-formatted responses.',
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
let fullContent = '';
|
|
81
|
-
for await (const chunk of responseStream) {
|
|
82
|
-
if (isAborted()) break;
|
|
83
|
-
const textChunk = chunk.text || '';
|
|
84
|
-
if (textChunk) {
|
|
85
|
-
fullContent += textChunk;
|
|
86
|
-
sendEvent('content_chunk', { delta: textChunk });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
sendEvent('content_end', {});
|
|
91
|
-
|
|
92
|
-
const summary = 'Generation completed successfully via Gemini API.';
|
|
93
|
-
sendEvent('summary', { summary });
|
|
94
|
-
|
|
95
|
-
const finalMessage: ChatMessageData = {
|
|
96
|
-
id: messageId,
|
|
97
|
-
role: 'ai',
|
|
98
|
-
date: `Today, ${timeStr}`,
|
|
99
|
-
timestamp: timeStr,
|
|
100
|
-
thinking: {
|
|
101
|
-
duration: '0.8s',
|
|
102
|
-
thought: thoughtText,
|
|
103
|
-
summary: 'Analyze context and stream response',
|
|
104
|
-
},
|
|
105
|
-
content: fullContent,
|
|
106
|
-
summary,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
sendEvent('done', { message: finalMessage });
|
|
110
|
-
await persistAiMessage(sessionId, finalMessage);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
37
|
// Handler for domain-aware simulated streaming with tool executions & chunked tokens
|
|
114
38
|
export async function handleSimulatedStreaming(options: {
|
|
115
39
|
prompt: string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path formatting for the Files panel's copy actions.
|
|
3
|
+
*
|
|
4
|
+
* `/api/fs/dir` emits every entry as a path relative to the listing's base
|
|
5
|
+
* directory — the workspace root, or the selected nested repo — and reports
|
|
6
|
+
* that base back as `root`. Copy Path rebuilds the absolute path from those
|
|
7
|
+
* two halves; a relative path on its own is not resolvable outside the panel
|
|
8
|
+
* (the client-side `rootPath` may be `~`-relative, rejected by the server, or
|
|
9
|
+
* absent entirely in mock mode).
|
|
10
|
+
*/
|
|
11
|
+
export function toAbsolutePath(baseDir: string | null | undefined, relPath: string): string {
|
|
12
|
+
// `/`- and `./`-prefixed raws are stripped; a dot-file name (`.env`) is not a prefix.
|
|
13
|
+
const rel = relPath.replace(/^(?:\.?[/\\])+/, '').replace(/[/\\]+$/, '');
|
|
14
|
+
const base = (baseDir ?? '').replace(/[/\\]+$/, '');
|
|
15
|
+
if (!base) return rel;
|
|
16
|
+
if (!rel) return base;
|
|
17
|
+
// Join with the base's own separator: `C:\proj` + `src` → `C:\proj\src`.
|
|
18
|
+
return `${base}${base.includes('\\') && !base.includes('/') ? '\\' : '/'}${rel}`;
|
|
19
|
+
}
|