gestalt-mobile 0.35.5 → 0.36.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 +60 -3
- package/dist/client/assets/index-9XMv2BvD.js +38 -0
- package/dist/client/assets/{index-CPQBJDsg.css → index-Bxv5Z2w4.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/client/features/chat/activity-summary.js +71 -0
- package/dist/server/client/features/chat/chat-projection.js +722 -0
- package/dist/server/client/features/chat/message-store.js +30 -0
- package/dist/server/server/app.js +1 -1
- package/dist/server/server/cli.js +8 -0
- package/dist/server/server/composition.contracts.js +158 -6
- package/dist/server/server/composition.js +266 -56
- package/dist/server/server/features/autopilot/register-routes.js +2 -2
- package/dist/server/server/features/autopilot/toggle/endpoint.js +5 -1
- package/dist/server/server/features/catalog/get-bootstrap/use-case.js +5 -3
- package/dist/server/server/features/sessions/get-session/endpoint.js +1 -1
- package/dist/server/server/features/sessions/list-models/endpoint.js +16 -0
- package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +8 -3
- package/dist/server/server/features/sessions/list-sessions/endpoint.js +1 -1
- package/dist/server/server/features/sessions/model/relay-session.js +2 -0
- package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +6 -2
- package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +7 -2
- package/dist/server/server/features/sessions/register-routes.js +8 -3
- package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
- package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
- package/dist/server/server/features/sessions/start-session/request.js +1 -0
- package/dist/server/server/features/sessions/start-session/use-case.js +21 -7
- package/dist/server/server/features/skills/list-available/endpoint.js +4 -2
- package/dist/server/server/platform/catalog/provider-model-catalog.js +15 -0
- package/dist/server/server/platform/kimi/kimi-activity-facts.js +88 -0
- package/dist/server/server/platform/kimi/kimi-errors.js +30 -0
- package/dist/server/server/platform/kimi/kimi-model-catalog.js +63 -0
- package/dist/server/server/platform/kimi/kimi-normalizer.js +196 -0
- package/dist/server/server/platform/kimi/kimi-recent-session-lister.js +68 -0
- package/dist/server/server/platform/kimi/kimi-server-request.js +143 -0
- package/dist/server/server/platform/kimi/kimi-session-runtime.js +653 -0
- package/dist/server/server/platform/kimi/kimi-share-dir.js +65 -0
- package/dist/server/server/platform/kimi/kimi-skill-catalog.js +79 -0
- package/dist/server/server/platform/kimi/kimi-skills-materializer.js +41 -0
- package/dist/server/server/platform/kimi/kimi-web-client.js +63 -0
- package/dist/server/server/platform/kimi/kimi-web-server-manager.js +215 -0
- package/dist/server/server/platform/kimi/kimi-ws-client.js +154 -0
- package/dist/server/server/platform/persistence/migrate.js +2 -0
- package/dist/server/server/platform/persistence/sqlite-session-repository.js +3 -2
- package/dist/server/server/platform/skills/cached-skill-catalog.js +0 -0
- package/dist/server/shared/contracts/llm-provider.js +6 -0
- package/package.json +4 -3
- package/dist/client/assets/index-SZM7lpix.js +0 -38
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export function appendUserMessage(messages, id, text, occurredAt) {
|
|
7
|
+
return [
|
|
8
|
+
...messages,
|
|
9
|
+
{ id, role: 'user', text, ...(occurredAt ? { occurredAt } : {}), complete: true },
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
export function applyDelta(messages, id, text, occurredAt) {
|
|
13
|
+
const previous = messages.find((message) => message.id === id);
|
|
14
|
+
return previous
|
|
15
|
+
? messages.map((message) => message.id === id ? { ...message, text: message.text + text } : message)
|
|
16
|
+
: [
|
|
17
|
+
...messages,
|
|
18
|
+
{
|
|
19
|
+
id,
|
|
20
|
+
role: 'assistant',
|
|
21
|
+
phase: 'commentary',
|
|
22
|
+
text,
|
|
23
|
+
...(occurredAt ? { occurredAt } : {}),
|
|
24
|
+
complete: false,
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
export function completeMessage(messages, id) {
|
|
29
|
+
return messages.map((message) => (message.id === id ? { ...message, complete: true } : message));
|
|
30
|
+
}
|
|
@@ -46,7 +46,7 @@ export async function buildApp(deps) {
|
|
|
46
46
|
registerGetBootstrap(app, deps.bootstrap);
|
|
47
47
|
registerSessionRoutes(app, deps);
|
|
48
48
|
if (deps.autopilot)
|
|
49
|
-
registerAutopilotRoutes(app, deps.autopilot, deps.sessionRoutes?.idempotency);
|
|
49
|
+
registerAutopilotRoutes(app, deps.autopilot, deps.sessionRoutes?.idempotency, deps.sessionRoutes ? (id) => deps.sessionRoutes?.find(id)?.provider : undefined);
|
|
50
50
|
if (deps.orgPlanAttention)
|
|
51
51
|
registerOrgPlanAttentionRoutes(app, deps.orgPlanAttention);
|
|
52
52
|
registerPlanRoutes(app, deps);
|
|
@@ -98,6 +98,12 @@ export async function probeCodexVersion() {
|
|
|
98
98
|
.then(({ stdout }) => stdout.trim() || null)
|
|
99
99
|
.catch(() => null);
|
|
100
100
|
}
|
|
101
|
+
/** Detects the kimi CLI exactly like the codex probe; absence is a soft status. */
|
|
102
|
+
export async function probeKimiVersion() {
|
|
103
|
+
return runFile('kimi', ['--version'])
|
|
104
|
+
.then(({ stdout }) => stdout.trim() || null)
|
|
105
|
+
.catch(() => null);
|
|
106
|
+
}
|
|
101
107
|
export function installShutdownHandlers(app, signalSource = process) {
|
|
102
108
|
let closing;
|
|
103
109
|
const shutdown = () => {
|
|
@@ -205,6 +211,7 @@ export async function runCli(dependencies = {}) {
|
|
|
205
211
|
if (!diagnosis.ok)
|
|
206
212
|
stderr.write('WARNING: Startup diagnostics reported a problem; Gestalt Mobile will continue so it can show recovery controls.\n');
|
|
207
213
|
const installedCodexVersion = await (dependencies.probeCodexVersion ?? probeCodexVersion)();
|
|
214
|
+
const installedKimiVersion = await (dependencies.probeKimiVersion ?? probeKimiVersion)();
|
|
208
215
|
const mobileVersion = await packageVersion(moduleUrl);
|
|
209
216
|
const app = await (dependencies.compose ?? composeRelayApp)({
|
|
210
217
|
root: config.root,
|
|
@@ -214,6 +221,7 @@ export async function runCli(dependencies = {}) {
|
|
|
214
221
|
staticDir: packagedClientDir(moduleUrl),
|
|
215
222
|
profiles: new LauncherProfileCatalog(),
|
|
216
223
|
installedCodexVersion,
|
|
224
|
+
installedKimiVersion,
|
|
217
225
|
componentVersions: componentVersions(mobileVersion, installedCodexVersion, dependencies.environment),
|
|
218
226
|
startAppServers: true,
|
|
219
227
|
homeDirectory: dependencies.homeDirectory,
|
|
@@ -21,6 +21,7 @@ import { workspaceId } from './platform/catalog/workspace-id.js';
|
|
|
21
21
|
import { planStatusDirectoryPath, planStatusFilePath, } from './platform/plans/filesystem-plan-status-source.js';
|
|
22
22
|
import { toOrgPlanAttentionToolResponse } from '../shared/contracts/org-plan-attention.js';
|
|
23
23
|
import { toOrgPlanCheckpointToolResponse } from '../shared/contracts/org-plan-checkpoint.js';
|
|
24
|
+
import { applyProjectionEvent, createChatProjection, } from '../client/features/chat/chat-projection.js';
|
|
24
25
|
function fakeAppServer(calls) {
|
|
25
26
|
return {
|
|
26
27
|
rpc: {
|
|
@@ -125,7 +126,7 @@ async function createComposedSession(app) {
|
|
|
125
126
|
const created = await app.inject({
|
|
126
127
|
method: 'POST',
|
|
127
128
|
url: '/api/sessions',
|
|
128
|
-
payload: { workspaceId: workspace.id, profile: 'default' },
|
|
129
|
+
payload: { workspaceId: workspace.id, profile: 'default', provider: 'codex' },
|
|
129
130
|
});
|
|
130
131
|
expect(created.statusCode).toBe(202);
|
|
131
132
|
return created.json().id;
|
|
@@ -2680,6 +2681,7 @@ describe('production composition', () => {
|
|
|
2680
2681
|
['DELETE', '/api/auth/devices/:deviceId', '/api/auth/devices/device-1', 'protected'],
|
|
2681
2682
|
['GET', '/api/bootstrap', '/api/bootstrap', 'protected'],
|
|
2682
2683
|
['HEAD', '/api/bootstrap', '/api/bootstrap', 'protected'],
|
|
2684
|
+
['POST', '/api/session-models/:provider', '/api/session-models/codex', 'protected'],
|
|
2683
2685
|
['POST', '/api/sessions', '/api/sessions', 'protected'],
|
|
2684
2686
|
['GET', '/api/sessions', '/api/sessions', 'protected'],
|
|
2685
2687
|
['HEAD', '/api/sessions', '/api/sessions', 'protected'],
|
|
@@ -3060,7 +3062,7 @@ describe('production composition', () => {
|
|
|
3060
3062
|
const created = await app.inject({
|
|
3061
3063
|
method: 'POST',
|
|
3062
3064
|
url: '/api/sessions',
|
|
3063
|
-
payload: { workspaceId: workspace.id, profile: 'default' },
|
|
3065
|
+
payload: { workspaceId: workspace.id, profile: 'default', provider: 'codex' },
|
|
3064
3066
|
});
|
|
3065
3067
|
expect(created.statusCode).toBe(202);
|
|
3066
3068
|
const restored = await app.inject({
|
|
@@ -3073,6 +3075,156 @@ describe('production composition', () => {
|
|
|
3073
3075
|
});
|
|
3074
3076
|
await app.close();
|
|
3075
3077
|
});
|
|
3078
|
+
it('keeps cold bootstrap passive while explicit Kimi session creation resolves a Kimi model', async () => {
|
|
3079
|
+
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
3080
|
+
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
3081
|
+
ownTemporaryPaths(root, dataDir);
|
|
3082
|
+
await mkdir(join(root, 'workspace'));
|
|
3083
|
+
const sessionModelCalls = [];
|
|
3084
|
+
const app = await composeAuthorizedApp({
|
|
3085
|
+
root,
|
|
3086
|
+
dataDir,
|
|
3087
|
+
relyingParty,
|
|
3088
|
+
installedCodexVersion: null,
|
|
3089
|
+
installedKimiVersion: '2.0.2',
|
|
3090
|
+
launchAppServer: () => fakeAppServer([]),
|
|
3091
|
+
sessionModelCatalog: {
|
|
3092
|
+
list: async (provider) => {
|
|
3093
|
+
sessionModelCalls.push(provider);
|
|
3094
|
+
return provider === 'kimi' ? ['k2-thinking'] : ['gpt-5.6-terra'];
|
|
3095
|
+
},
|
|
3096
|
+
},
|
|
3097
|
+
profiles: {
|
|
3098
|
+
list: async () => [],
|
|
3099
|
+
require: async () => {
|
|
3100
|
+
throw new Error('CODEX_LAUNCHER_UNAVAILABLE');
|
|
3101
|
+
},
|
|
3102
|
+
},
|
|
3103
|
+
});
|
|
3104
|
+
const bootstrap = await app.inject('/api/bootstrap');
|
|
3105
|
+
const workspace = bootstrap
|
|
3106
|
+
.json()
|
|
3107
|
+
.workspaces[0]?.children.find((item) => item.name === 'workspace');
|
|
3108
|
+
expect(bootstrap.json().models.kimi).toEqual([]);
|
|
3109
|
+
expect(sessionModelCalls).toEqual([]);
|
|
3110
|
+
const created = await app.inject({
|
|
3111
|
+
method: 'POST',
|
|
3112
|
+
url: '/api/sessions',
|
|
3113
|
+
payload: { workspaceId: workspace.id, profile: 'default', provider: 'kimi' },
|
|
3114
|
+
});
|
|
3115
|
+
expect(created.statusCode).toBe(202);
|
|
3116
|
+
expect(created.json()).toMatchObject({ provider: 'kimi', model: 'k2-thinking' });
|
|
3117
|
+
expect(sessionModelCalls).toEqual(['kimi']);
|
|
3118
|
+
await app.close();
|
|
3119
|
+
});
|
|
3120
|
+
it('keeps real server listen passive when Kimi is installed', async () => {
|
|
3121
|
+
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
3122
|
+
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
3123
|
+
ownTemporaryPaths(root, dataDir);
|
|
3124
|
+
await mkdir(join(root, 'workspace'));
|
|
3125
|
+
const ensure = vi.fn(async () => {
|
|
3126
|
+
throw new Error('passive startup must not ensure Kimi');
|
|
3127
|
+
});
|
|
3128
|
+
const app = await composeAuthorizedApp({
|
|
3129
|
+
root,
|
|
3130
|
+
dataDir,
|
|
3131
|
+
relyingParty,
|
|
3132
|
+
installedCodexVersion: null,
|
|
3133
|
+
installedKimiVersion: '2.0.2',
|
|
3134
|
+
startAppServers: true,
|
|
3135
|
+
kimiServerManager: {
|
|
3136
|
+
ensure,
|
|
3137
|
+
get: () => null,
|
|
3138
|
+
list: () => [],
|
|
3139
|
+
stopAll: async () => { },
|
|
3140
|
+
},
|
|
3141
|
+
profiles: {
|
|
3142
|
+
list: async () => [],
|
|
3143
|
+
require: async () => {
|
|
3144
|
+
throw new Error('CODEX_LAUNCHER_UNAVAILABLE');
|
|
3145
|
+
},
|
|
3146
|
+
},
|
|
3147
|
+
});
|
|
3148
|
+
await app.listen({ host: '127.0.0.1', port: 0 });
|
|
3149
|
+
expect(ensure).not.toHaveBeenCalled();
|
|
3150
|
+
await app.close();
|
|
3151
|
+
});
|
|
3152
|
+
it('delivers a realistic Codex answer notification through the journal to the chat projection', async () => {
|
|
3153
|
+
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
3154
|
+
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
3155
|
+
ownTemporaryPaths(root, dataDir);
|
|
3156
|
+
await mkdir(join(root, 'workspace'));
|
|
3157
|
+
const handles = [];
|
|
3158
|
+
const app = await composeAuthorizedApp({
|
|
3159
|
+
root,
|
|
3160
|
+
dataDir,
|
|
3161
|
+
relyingParty,
|
|
3162
|
+
installedCodexVersion: 'codex-cli 0.144.3',
|
|
3163
|
+
startAppServers: true,
|
|
3164
|
+
launchAppServer: liveAppServer(handles),
|
|
3165
|
+
profiles: {
|
|
3166
|
+
list: async () => [],
|
|
3167
|
+
require: async () => ({
|
|
3168
|
+
name: 'default',
|
|
3169
|
+
state: 'ok',
|
|
3170
|
+
status: 'ready',
|
|
3171
|
+
}),
|
|
3172
|
+
},
|
|
3173
|
+
});
|
|
3174
|
+
const sessionId = await createComposedSession(app);
|
|
3175
|
+
const handle = handles.find((candidate) => candidate.calls.includes('thread/start'));
|
|
3176
|
+
const started = await app.inject({
|
|
3177
|
+
method: 'POST',
|
|
3178
|
+
url: `/api/sessions/${sessionId}/turns`,
|
|
3179
|
+
payload: { text: 'give the answer' },
|
|
3180
|
+
});
|
|
3181
|
+
expect(started.statusCode).toBe(202);
|
|
3182
|
+
const turnId = started.json().activeTurnId;
|
|
3183
|
+
const threadId = (await app.inject(`/api/sessions/${sessionId}`)).json().threadId;
|
|
3184
|
+
await app.listen({ host: '127.0.0.1', port: 0 });
|
|
3185
|
+
const address = app.server.address();
|
|
3186
|
+
if (!address || typeof address === 'string')
|
|
3187
|
+
throw new Error('Expected TCP listener');
|
|
3188
|
+
const socket = new WebSocket(`ws://127.0.0.1:${address.port}/api/sessions/${sessionId}/events?after=0`, {
|
|
3189
|
+
headers: {
|
|
3190
|
+
origin: relyingParty.publicOrigin,
|
|
3191
|
+
cookie: 'gestalt_mobile_session=test-session',
|
|
3192
|
+
},
|
|
3193
|
+
});
|
|
3194
|
+
const messages = [];
|
|
3195
|
+
socket.on('message', (data) => messages.push(JSON.parse(String(data))));
|
|
3196
|
+
await once(socket, 'open');
|
|
3197
|
+
handle.notify({
|
|
3198
|
+
method: 'item/completed',
|
|
3199
|
+
params: {
|
|
3200
|
+
threadId,
|
|
3201
|
+
turnId,
|
|
3202
|
+
item: {
|
|
3203
|
+
id: 'answer-1',
|
|
3204
|
+
type: 'agentMessage',
|
|
3205
|
+
phase: 'final_answer',
|
|
3206
|
+
text: 'The provider boundary is intact.',
|
|
3207
|
+
},
|
|
3208
|
+
},
|
|
3209
|
+
});
|
|
3210
|
+
await vi.waitFor(() => expect(messages.some((message) => message.event.type === 'agentMessageCompleted')).toBe(true));
|
|
3211
|
+
const projection = messages
|
|
3212
|
+
.map((message) => message.event)
|
|
3213
|
+
.sort((left, right) => left.sequence - right.sequence)
|
|
3214
|
+
.reduce((current, event) => applyProjectionEvent(current, event), createChatProjection(sessionId));
|
|
3215
|
+
expect(projection.messages).toEqual([
|
|
3216
|
+
expect.objectContaining({
|
|
3217
|
+
id: 'assistant:answer-1',
|
|
3218
|
+
role: 'assistant',
|
|
3219
|
+
text: 'The provider boundary is intact.',
|
|
3220
|
+
phase: 'final_answer',
|
|
3221
|
+
complete: true,
|
|
3222
|
+
turnId,
|
|
3223
|
+
}),
|
|
3224
|
+
]);
|
|
3225
|
+
socket.close();
|
|
3226
|
+
await app.close();
|
|
3227
|
+
});
|
|
3076
3228
|
it('hands a replayed session-owned plan replacement off to its live close event exactly once', async () => {
|
|
3077
3229
|
const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
|
|
3078
3230
|
const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
|
|
@@ -3110,7 +3262,7 @@ describe('production composition', () => {
|
|
|
3110
3262
|
const created = await app.inject({
|
|
3111
3263
|
method: 'POST',
|
|
3112
3264
|
url: '/api/sessions',
|
|
3113
|
-
payload: { workspaceId: workspace.id, profile: 'default' },
|
|
3265
|
+
payload: { workspaceId: workspace.id, profile: 'default', provider: 'codex' },
|
|
3114
3266
|
});
|
|
3115
3267
|
const sessionId = created.json().id;
|
|
3116
3268
|
await writeFile(planStatusFilePath(planStatusDirectoryPath(workspacePath, sessionId), planPath), JSON.stringify({
|
|
@@ -3234,7 +3386,7 @@ describe('production composition', () => {
|
|
|
3234
3386
|
const created = await first.inject({
|
|
3235
3387
|
method: 'POST',
|
|
3236
3388
|
url: '/api/sessions',
|
|
3237
|
-
payload: { workspaceId: workspace.id, profile: 'default' },
|
|
3389
|
+
payload: { workspaceId: workspace.id, profile: 'default', provider: 'codex' },
|
|
3238
3390
|
});
|
|
3239
3391
|
expect(created.statusCode).toBe(202);
|
|
3240
3392
|
expect(firstCalls).toEqual([
|
|
@@ -3316,7 +3468,7 @@ describe('production composition', () => {
|
|
|
3316
3468
|
await app.inject({
|
|
3317
3469
|
method: 'POST',
|
|
3318
3470
|
url: '/api/sessions',
|
|
3319
|
-
payload: { workspaceId: workspace.id, profile: 'default' },
|
|
3471
|
+
payload: { workspaceId: workspace.id, profile: 'default', provider: 'codex' },
|
|
3320
3472
|
});
|
|
3321
3473
|
await app.close();
|
|
3322
3474
|
// Model and skill catalogs run for bootstrap and session start; the active child closes with the relay.
|
|
@@ -3387,7 +3539,7 @@ describe('production composition', () => {
|
|
|
3387
3539
|
const created = await app.inject({
|
|
3388
3540
|
method: 'POST',
|
|
3389
3541
|
url: '/api/sessions',
|
|
3390
|
-
payload: { workspaceId: workspace.id, profile: 'default' },
|
|
3542
|
+
payload: { workspaceId: workspace.id, profile: 'default', provider: 'codex' },
|
|
3391
3543
|
});
|
|
3392
3544
|
const sessionId = created.json().id;
|
|
3393
3545
|
const handle = handles.find((candidate) => candidate.calls.includes('thread/start'));
|