gestalt-mobile 0.35.5 → 0.36.1

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.
Files changed (51) hide show
  1. package/README.md +60 -3
  2. package/dist/client/assets/index-9XMv2BvD.js +38 -0
  3. package/dist/client/assets/{index-CPQBJDsg.css → index-Bxv5Z2w4.css} +1 -1
  4. package/dist/client/index.html +2 -2
  5. package/dist/server/client/features/chat/activity-summary.js +71 -0
  6. package/dist/server/client/features/chat/chat-projection.js +722 -0
  7. package/dist/server/client/features/chat/message-store.js +30 -0
  8. package/dist/server/server/app.js +1 -1
  9. package/dist/server/server/cli.js +8 -0
  10. package/dist/server/server/composition.contracts.js +193 -10
  11. package/dist/server/server/composition.js +283 -59
  12. package/dist/server/server/features/autopilot/register-routes.js +2 -2
  13. package/dist/server/server/features/autopilot/toggle/endpoint.js +5 -1
  14. package/dist/server/server/features/catalog/get-bootstrap/use-case.js +5 -3
  15. package/dist/server/server/features/org-plan-checkpoint/application/validate.js +40 -0
  16. package/dist/server/server/features/sessions/get-session/endpoint.js +1 -1
  17. package/dist/server/server/features/sessions/list-models/endpoint.js +16 -0
  18. package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +8 -3
  19. package/dist/server/server/features/sessions/list-sessions/endpoint.js +1 -1
  20. package/dist/server/server/features/sessions/model/relay-session.js +2 -0
  21. package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +6 -2
  22. package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +7 -2
  23. package/dist/server/server/features/sessions/register-routes.js +8 -3
  24. package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
  25. package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
  26. package/dist/server/server/features/sessions/start-session/request.js +1 -0
  27. package/dist/server/server/features/sessions/start-session/use-case.js +21 -7
  28. package/dist/server/server/features/skills/list-available/endpoint.js +4 -2
  29. package/dist/server/server/platform/catalog/provider-model-catalog.js +15 -0
  30. package/dist/server/server/platform/codex/server-request.js +14 -0
  31. package/dist/server/server/platform/codex/session-runtime.js +5 -1
  32. package/dist/server/server/platform/kimi/kimi-activity-facts.js +88 -0
  33. package/dist/server/server/platform/kimi/kimi-errors.js +30 -0
  34. package/dist/server/server/platform/kimi/kimi-model-catalog.js +63 -0
  35. package/dist/server/server/platform/kimi/kimi-normalizer.js +196 -0
  36. package/dist/server/server/platform/kimi/kimi-recent-session-lister.js +68 -0
  37. package/dist/server/server/platform/kimi/kimi-server-request.js +143 -0
  38. package/dist/server/server/platform/kimi/kimi-session-runtime.js +653 -0
  39. package/dist/server/server/platform/kimi/kimi-share-dir.js +65 -0
  40. package/dist/server/server/platform/kimi/kimi-skill-catalog.js +79 -0
  41. package/dist/server/server/platform/kimi/kimi-skills-materializer.js +41 -0
  42. package/dist/server/server/platform/kimi/kimi-web-client.js +63 -0
  43. package/dist/server/server/platform/kimi/kimi-web-server-manager.js +215 -0
  44. package/dist/server/server/platform/kimi/kimi-ws-client.js +154 -0
  45. package/dist/server/server/platform/persistence/migrate.js +2 -0
  46. package/dist/server/server/platform/persistence/sqlite-session-repository.js +3 -2
  47. package/dist/server/server/platform/skills/cached-skill-catalog.js +0 -0
  48. package/dist/server/shared/contracts/llm-provider.js +6 -0
  49. package/dist/server/shared/contracts/org-plan-attention.js +15 -1
  50. package/package.json +4 -3
  51. 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;
@@ -195,7 +196,7 @@ async function createProductionAutopilotFixture(overrides = {}) {
195
196
  await installAutopilotPlan(app, sessionId, workspacePath, 'autopilot');
196
197
  return { app, dataDir, handles, root, sessionId, workspacePath };
197
198
  }
198
- async function holdCheckpointRequest(fixture, timers, requestId, expectHeld = true) {
199
+ async function holdCheckpointRequest(fixture, timers, requestId, expectHeld = true, compact = false) {
199
200
  timers.find((timer) => !timer.cancelled).callback();
200
201
  await vi.waitFor(() => expect(fixture.handles.flatMap((candidate) => candidate.requests.filter((request) => request.method === 'turn/start'))).toHaveLength(1));
201
202
  const handle = fixture.handles.find((candidate) => candidate.requests.some((request) => request.method === 'turn/start'));
@@ -217,7 +218,18 @@ async function holdCheckpointRequest(fixture, timers, requestId, expectHeld = tr
217
218
  const startsBefore = fixture.handles
218
219
  .flatMap((candidate) => candidate.requests)
219
220
  .filter((request) => request.method === 'turn/start').length;
220
- const response = handle.request(l2CheckpointCall(requestId, threadId, turnId, planIdentity));
221
+ const response = handle.request(compact
222
+ ? {
223
+ id: requestId,
224
+ method: 'item/tool/call',
225
+ params: {
226
+ threadId,
227
+ turnId,
228
+ tool: 'gestalt_org_plan_checkpoint',
229
+ arguments: { kind: 'l2Completed' },
230
+ },
231
+ }
232
+ : l2CheckpointCall(requestId, threadId, turnId, planIdentity));
221
233
  // The caller can be synchronously rejected by a coordinator exception.
222
234
  // Mark it observed immediately; callers still assert its exact outcome.
223
235
  void response.catch(() => { });
@@ -914,6 +926,28 @@ describe('production composition', () => {
914
926
  .filter((request) => request.method === 'turn/start')).toHaveLength(2));
915
927
  await fixture.app.close();
916
928
  });
929
+ it('expands and acknowledges a compact checkpoint signal from the active root', async () => {
930
+ const timers = [];
931
+ const fixture = await createProductionAutopilotFixture({
932
+ autopilotSchedule: (callback) => {
933
+ const timer = { callback, cancelled: false };
934
+ timers.push(timer);
935
+ return () => {
936
+ timer.cancelled = true;
937
+ };
938
+ },
939
+ });
940
+ const { handle, response } = await holdCheckpointRequest(fixture, timers, 812, true, true);
941
+ handle.responseSettled({ id: 812, outcome: 'resultWritten' });
942
+ await expect(response).resolves.toEqual(toOrgPlanCheckpointToolResponse());
943
+ const database = new DatabaseSync(join(fixture.dataDir, 'relay.sqlite'));
944
+ const checkpoint = database
945
+ .prepare("SELECT count(*) AS count FROM session_events WHERE session_id = ? AND type = 'org-plan.step-checkpointed'")
946
+ .get(fixture.sessionId);
947
+ database.close();
948
+ expect(checkpoint.count).toBe(1);
949
+ await fixture.app.close();
950
+ });
917
951
  it('leaves an unsupported checkpoint exception before persistence inert', async () => {
918
952
  let coordinator;
919
953
  const timers = [];
@@ -2680,6 +2714,7 @@ describe('production composition', () => {
2680
2714
  ['DELETE', '/api/auth/devices/:deviceId', '/api/auth/devices/device-1', 'protected'],
2681
2715
  ['GET', '/api/bootstrap', '/api/bootstrap', 'protected'],
2682
2716
  ['HEAD', '/api/bootstrap', '/api/bootstrap', 'protected'],
2717
+ ['POST', '/api/session-models/:provider', '/api/session-models/codex', 'protected'],
2683
2718
  ['POST', '/api/sessions', '/api/sessions', 'protected'],
2684
2719
  ['GET', '/api/sessions', '/api/sessions', 'protected'],
2685
2720
  ['HEAD', '/api/sessions', '/api/sessions', 'protected'],
@@ -3060,7 +3095,7 @@ describe('production composition', () => {
3060
3095
  const created = await app.inject({
3061
3096
  method: 'POST',
3062
3097
  url: '/api/sessions',
3063
- payload: { workspaceId: workspace.id, profile: 'default' },
3098
+ payload: { workspaceId: workspace.id, profile: 'default', provider: 'codex' },
3064
3099
  });
3065
3100
  expect(created.statusCode).toBe(202);
3066
3101
  const restored = await app.inject({
@@ -3073,6 +3108,156 @@ describe('production composition', () => {
3073
3108
  });
3074
3109
  await app.close();
3075
3110
  });
3111
+ it('keeps cold bootstrap passive while explicit Kimi session creation resolves a Kimi model', async () => {
3112
+ const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
3113
+ const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
3114
+ ownTemporaryPaths(root, dataDir);
3115
+ await mkdir(join(root, 'workspace'));
3116
+ const sessionModelCalls = [];
3117
+ const app = await composeAuthorizedApp({
3118
+ root,
3119
+ dataDir,
3120
+ relyingParty,
3121
+ installedCodexVersion: null,
3122
+ installedKimiVersion: '2.0.2',
3123
+ launchAppServer: () => fakeAppServer([]),
3124
+ sessionModelCatalog: {
3125
+ list: async (provider) => {
3126
+ sessionModelCalls.push(provider);
3127
+ return provider === 'kimi' ? ['k2-thinking'] : ['gpt-5.6-terra'];
3128
+ },
3129
+ },
3130
+ profiles: {
3131
+ list: async () => [],
3132
+ require: async () => {
3133
+ throw new Error('CODEX_LAUNCHER_UNAVAILABLE');
3134
+ },
3135
+ },
3136
+ });
3137
+ const bootstrap = await app.inject('/api/bootstrap');
3138
+ const workspace = bootstrap
3139
+ .json()
3140
+ .workspaces[0]?.children.find((item) => item.name === 'workspace');
3141
+ expect(bootstrap.json().models.kimi).toEqual([]);
3142
+ expect(sessionModelCalls).toEqual([]);
3143
+ const created = await app.inject({
3144
+ method: 'POST',
3145
+ url: '/api/sessions',
3146
+ payload: { workspaceId: workspace.id, profile: 'default', provider: 'kimi' },
3147
+ });
3148
+ expect(created.statusCode).toBe(202);
3149
+ expect(created.json()).toMatchObject({ provider: 'kimi', model: 'k2-thinking' });
3150
+ expect(sessionModelCalls).toEqual(['kimi']);
3151
+ await app.close();
3152
+ });
3153
+ it('keeps real server listen passive when Kimi is installed', async () => {
3154
+ const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
3155
+ const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
3156
+ ownTemporaryPaths(root, dataDir);
3157
+ await mkdir(join(root, 'workspace'));
3158
+ const ensure = vi.fn(async () => {
3159
+ throw new Error('passive startup must not ensure Kimi');
3160
+ });
3161
+ const app = await composeAuthorizedApp({
3162
+ root,
3163
+ dataDir,
3164
+ relyingParty,
3165
+ installedCodexVersion: null,
3166
+ installedKimiVersion: '2.0.2',
3167
+ startAppServers: true,
3168
+ kimiServerManager: {
3169
+ ensure,
3170
+ get: () => null,
3171
+ list: () => [],
3172
+ stopAll: async () => { },
3173
+ },
3174
+ profiles: {
3175
+ list: async () => [],
3176
+ require: async () => {
3177
+ throw new Error('CODEX_LAUNCHER_UNAVAILABLE');
3178
+ },
3179
+ },
3180
+ });
3181
+ await app.listen({ host: '127.0.0.1', port: 0 });
3182
+ expect(ensure).not.toHaveBeenCalled();
3183
+ await app.close();
3184
+ });
3185
+ it('delivers a realistic Codex answer notification through the journal to the chat projection', async () => {
3186
+ const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
3187
+ const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
3188
+ ownTemporaryPaths(root, dataDir);
3189
+ await mkdir(join(root, 'workspace'));
3190
+ const handles = [];
3191
+ const app = await composeAuthorizedApp({
3192
+ root,
3193
+ dataDir,
3194
+ relyingParty,
3195
+ installedCodexVersion: 'codex-cli 0.144.3',
3196
+ startAppServers: true,
3197
+ launchAppServer: liveAppServer(handles),
3198
+ profiles: {
3199
+ list: async () => [],
3200
+ require: async () => ({
3201
+ name: 'default',
3202
+ state: 'ok',
3203
+ status: 'ready',
3204
+ }),
3205
+ },
3206
+ });
3207
+ const sessionId = await createComposedSession(app);
3208
+ const handle = handles.find((candidate) => candidate.calls.includes('thread/start'));
3209
+ const started = await app.inject({
3210
+ method: 'POST',
3211
+ url: `/api/sessions/${sessionId}/turns`,
3212
+ payload: { text: 'give the answer' },
3213
+ });
3214
+ expect(started.statusCode).toBe(202);
3215
+ const turnId = started.json().activeTurnId;
3216
+ const threadId = (await app.inject(`/api/sessions/${sessionId}`)).json().threadId;
3217
+ await app.listen({ host: '127.0.0.1', port: 0 });
3218
+ const address = app.server.address();
3219
+ if (!address || typeof address === 'string')
3220
+ throw new Error('Expected TCP listener');
3221
+ const socket = new WebSocket(`ws://127.0.0.1:${address.port}/api/sessions/${sessionId}/events?after=0`, {
3222
+ headers: {
3223
+ origin: relyingParty.publicOrigin,
3224
+ cookie: 'gestalt_mobile_session=test-session',
3225
+ },
3226
+ });
3227
+ const messages = [];
3228
+ socket.on('message', (data) => messages.push(JSON.parse(String(data))));
3229
+ await once(socket, 'open');
3230
+ handle.notify({
3231
+ method: 'item/completed',
3232
+ params: {
3233
+ threadId,
3234
+ turnId,
3235
+ item: {
3236
+ id: 'answer-1',
3237
+ type: 'agentMessage',
3238
+ phase: 'final_answer',
3239
+ text: 'The provider boundary is intact.',
3240
+ },
3241
+ },
3242
+ });
3243
+ await vi.waitFor(() => expect(messages.some((message) => message.event.type === 'agentMessageCompleted')).toBe(true));
3244
+ const projection = messages
3245
+ .map((message) => message.event)
3246
+ .sort((left, right) => left.sequence - right.sequence)
3247
+ .reduce((current, event) => applyProjectionEvent(current, event), createChatProjection(sessionId));
3248
+ expect(projection.messages).toEqual([
3249
+ expect.objectContaining({
3250
+ id: 'assistant:answer-1',
3251
+ role: 'assistant',
3252
+ text: 'The provider boundary is intact.',
3253
+ phase: 'final_answer',
3254
+ complete: true,
3255
+ turnId,
3256
+ }),
3257
+ ]);
3258
+ socket.close();
3259
+ await app.close();
3260
+ });
3076
3261
  it('hands a replayed session-owned plan replacement off to its live close event exactly once', async () => {
3077
3262
  const root = await mkdtemp(join(tmpdir(), 'gestalt-mobile-root-'));
3078
3263
  const dataDir = await mkdtemp(join(tmpdir(), 'gestalt-mobile-state-'));
@@ -3110,7 +3295,7 @@ describe('production composition', () => {
3110
3295
  const created = await app.inject({
3111
3296
  method: 'POST',
3112
3297
  url: '/api/sessions',
3113
- payload: { workspaceId: workspace.id, profile: 'default' },
3298
+ payload: { workspaceId: workspace.id, profile: 'default', provider: 'codex' },
3114
3299
  });
3115
3300
  const sessionId = created.json().id;
3116
3301
  await writeFile(planStatusFilePath(planStatusDirectoryPath(workspacePath, sessionId), planPath), JSON.stringify({
@@ -3234,7 +3419,7 @@ describe('production composition', () => {
3234
3419
  const created = await first.inject({
3235
3420
  method: 'POST',
3236
3421
  url: '/api/sessions',
3237
- payload: { workspaceId: workspace.id, profile: 'default' },
3422
+ payload: { workspaceId: workspace.id, profile: 'default', provider: 'codex' },
3238
3423
  });
3239
3424
  expect(created.statusCode).toBe(202);
3240
3425
  expect(firstCalls).toEqual([
@@ -3316,7 +3501,7 @@ describe('production composition', () => {
3316
3501
  await app.inject({
3317
3502
  method: 'POST',
3318
3503
  url: '/api/sessions',
3319
- payload: { workspaceId: workspace.id, profile: 'default' },
3504
+ payload: { workspaceId: workspace.id, profile: 'default', provider: 'codex' },
3320
3505
  });
3321
3506
  await app.close();
3322
3507
  // Model and skill catalogs run for bootstrap and session start; the active child closes with the relay.
@@ -3387,7 +3572,7 @@ describe('production composition', () => {
3387
3572
  const created = await app.inject({
3388
3573
  method: 'POST',
3389
3574
  url: '/api/sessions',
3390
- payload: { workspaceId: workspace.id, profile: 'default' },
3575
+ payload: { workspaceId: workspace.id, profile: 'default', provider: 'codex' },
3391
3576
  });
3392
3577
  const sessionId = created.json().id;
3393
3578
  const handle = handles.find((candidate) => candidate.calls.includes('thread/start'));
@@ -3500,8 +3685,6 @@ describe('production composition', () => {
3500
3685
  tool: 'gestalt_org_plan_attention',
3501
3686
  arguments: {
3502
3687
  reason: 'permissionRequired',
3503
- summary: 'A protected release needs approval.',
3504
- requestedAction: 'Grant the release permission.',
3505
3688
  resumeCondition: 'permissionGranted',
3506
3689
  },
3507
3690
  },