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.
Files changed (47) 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 +158 -6
  11. package/dist/server/server/composition.js +266 -56
  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/sessions/get-session/endpoint.js +1 -1
  16. package/dist/server/server/features/sessions/list-models/endpoint.js +16 -0
  17. package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +8 -3
  18. package/dist/server/server/features/sessions/list-sessions/endpoint.js +1 -1
  19. package/dist/server/server/features/sessions/model/relay-session.js +2 -0
  20. package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +6 -2
  21. package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +7 -2
  22. package/dist/server/server/features/sessions/register-routes.js +8 -3
  23. package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
  24. package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
  25. package/dist/server/server/features/sessions/start-session/request.js +1 -0
  26. package/dist/server/server/features/sessions/start-session/use-case.js +21 -7
  27. package/dist/server/server/features/skills/list-available/endpoint.js +4 -2
  28. package/dist/server/server/platform/catalog/provider-model-catalog.js +15 -0
  29. package/dist/server/server/platform/kimi/kimi-activity-facts.js +88 -0
  30. package/dist/server/server/platform/kimi/kimi-errors.js +30 -0
  31. package/dist/server/server/platform/kimi/kimi-model-catalog.js +63 -0
  32. package/dist/server/server/platform/kimi/kimi-normalizer.js +196 -0
  33. package/dist/server/server/platform/kimi/kimi-recent-session-lister.js +68 -0
  34. package/dist/server/server/platform/kimi/kimi-server-request.js +143 -0
  35. package/dist/server/server/platform/kimi/kimi-session-runtime.js +653 -0
  36. package/dist/server/server/platform/kimi/kimi-share-dir.js +65 -0
  37. package/dist/server/server/platform/kimi/kimi-skill-catalog.js +79 -0
  38. package/dist/server/server/platform/kimi/kimi-skills-materializer.js +41 -0
  39. package/dist/server/server/platform/kimi/kimi-web-client.js +63 -0
  40. package/dist/server/server/platform/kimi/kimi-web-server-manager.js +215 -0
  41. package/dist/server/server/platform/kimi/kimi-ws-client.js +154 -0
  42. package/dist/server/server/platform/persistence/migrate.js +2 -0
  43. package/dist/server/server/platform/persistence/sqlite-session-repository.js +3 -2
  44. package/dist/server/server/platform/skills/cached-skill-catalog.js +0 -0
  45. package/dist/server/shared/contracts/llm-provider.js +6 -0
  46. package/package.json +4 -3
  47. 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'));