@dotdrelle/wiki-manager 0.11.4 → 0.11.7

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.
@@ -1,6 +1,6 @@
1
1
  import { writeFileSync } from 'node:fs';
2
2
  import { isAbsolute, relative, resolve, sep } from 'node:path';
3
- import { createMemo, createSignal, onCleanup } from 'solid-js';
3
+ import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
4
4
  import { formatMcpToolResult, callMcpTool } from '../core/mcp.js';
5
5
  import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
6
6
  import { formatPlanStatus, formatCompletedActivities } from '../core/plan.js';
@@ -14,12 +14,13 @@ import {
14
14
  completionDescription,
15
15
  conversationMessages,
16
16
  createSession,
17
+ runtimeUnavailableAgentMessage,
17
18
  } from './repl.js';
18
19
  import { useAgent } from './useAgent';
19
20
 
20
- function runtimeStatusText(status: 'disabled' | 'connected' | 'disconnected', runStatus: string): string {
21
- if (status === 'connected') return `runtime ${runStatus}`;
22
- if (status === 'disconnected') return 'runtime offline';
21
+ function runtimeStatusText(status: 'disabled' | 'connected' | 'disconnected', runStatus: string, reason: string | null, workspace: string | null): string {
22
+ if (status === 'connected') return `runtime: connected (${workspace ?? 'no workspace'}) ${runStatus}`;
23
+ if (status === 'disconnected') return reason ? `runtime: disconnected: ${reason}` : 'runtime offline';
23
24
  return 'runtime off';
24
25
  }
25
26
 
@@ -74,13 +75,21 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
74
75
  const addLog = (line: string) => {
75
76
  setLogs((items) => [...items, `${new Date().toLocaleTimeString()} ${line}`].slice(-200));
76
77
  };
78
+ const runtimeUnavailableReason = createMemo(() => {
79
+ if (props.runtime?.url) return null;
80
+ const reason = props.runtime?.error ?? props.runtime?.unavailableReason ?? props.runtime?.reason ?? null;
81
+ return reason ? String(reason) : null;
82
+ });
77
83
  if (props.runtime?.url) addLog(`runtime: connected ${props.runtime.url}${props.runtime.started ? ' (started)' : ''}`);
84
+ else if (props.runtime?.error) addLog(`runtime: unavailable — ${props.runtime.error}`);
85
+ else if (runtimeUnavailableReason()) addLog(`runtime: disconnected: ${runtimeUnavailableReason()}`);
78
86
  const agent = useAgent({
79
87
  agent: props.agent,
80
88
  packageJson: props.packageJson,
81
89
  session,
82
90
  chatMode,
83
91
  runtimeUrl: props.runtime?.url ?? null,
92
+ runtimeUnavailableReason: runtimeUnavailableReason(),
84
93
  refresh,
85
94
  addLog,
86
95
  onRuntimeAccepted: () => {
@@ -97,23 +106,22 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
97
106
  );
98
107
  const localFallbackActive = createMemo(() => !props.runtime?.url || runtimeStatus() === 'disconnected');
99
108
 
109
+ // Runtime conversation entries are merged into conversationMessages(session)
110
+ // (see mergeRuntimeConversation below) as they arrive, so they land in true
111
+ // chronological order alongside local command output (/status, /use, ...)
112
+ // instead of being reconstructed as two separately-ordered blocks at render
113
+ // time — that used to put every local command before the whole runtime
114
+ // history regardless of when each one actually happened.
100
115
  const messages = createMemo(() => {
101
116
  version();
117
+ return [...conversationMessages(session)].slice(-200);
118
+ });
119
+ createEffect(() => {
102
120
  const runtimeConversation = runtimeState()?.conversation;
103
- const localCommands = conversationMessages(session)
104
- .filter((message: any) => message.role === 'command')
105
- .map((message: any) => ({
106
- role: 'command',
107
- content: String(message.content ?? ''),
108
- }));
109
121
  if (Array.isArray(runtimeConversation) && runtimeConversation.length > 0) {
110
- const runtimeMessages = runtimeConversation.map((message: any) => ({
111
- role: message.role === 'assistant' ? 'donna' : message.role,
112
- content: String(message.content ?? ''),
113
- }));
114
- return [...runtimeMessages, ...localCommands].slice(-200);
122
+ setShowWelcome(false);
123
+ setConversationScroll(0);
115
124
  }
116
- return [...conversationMessages(session)];
117
125
  });
118
126
  const prompt = createMemo(() => {
119
127
  version();
@@ -133,9 +141,14 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
133
141
  session.wikirc?.profile ? session.wikirc.profile : 'no wikirc',
134
142
  session.language ? session.language : 'no language',
135
143
  session.llm ? 'llm ready' : 'llm limited',
136
- runtimeStatusText(runtimeStatus(), runtimeRunStatus()),
144
+ runtimeStatusText(runtimeStatus(), runtimeRunStatus(), runtimeUnavailableReason(), session.workspace),
137
145
  ].join(' ');
138
146
  });
147
+ const runtimeHint = createMemo(() => {
148
+ version();
149
+ if (props.runtime?.url) return null;
150
+ return runtimeUnavailableAgentMessage({ error: runtimeUnavailableReason() ?? 'runtime introuvable' });
151
+ });
139
152
  const matchContext = createMemo(() => {
140
153
  if (input() === dismissedSlashInput()) return null;
141
154
  return completionContext(input(), session);
@@ -263,10 +276,54 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
263
276
  return [...logs(), ...tagged].slice(-200);
264
277
  });
265
278
 
279
+ // Index-aligned with each workspace's runtimeConversation array — not just
280
+ // a length count. The server sometimes finalizes a streaming placeholder
281
+ // (queued during a tool-calling turn, never closed out because that turn's
282
+ // reply had no final text) by overwriting the *same* conversation entry's
283
+ // content on a later turn, without the array growing. A length-based diff
284
+ // would miss that update entirely and leave the stale placeholder text
285
+ // shown forever; tracking local entry references per index picks it up.
286
+ const runtimeConversationRefsByWorkspace = new Map<string, any[]>();
287
+ function mergeRuntimeConversation(state: any) {
288
+ const workspace = (session as any).workspace || '__global__';
289
+ const runtimeConversation = Array.isArray(state?.conversation) ? state.conversation : [];
290
+ if (runtimeConversation.length === 0) return;
291
+ const target = conversationMessages(session);
292
+ const backed = runtimeConversationRefsByWorkspace.get(workspace) ?? [];
293
+ runtimeConversationRefsByWorkspace.set(workspace, backed);
294
+ for (let i = 0; i < runtimeConversation.length; i += 1) {
295
+ const message = runtimeConversation[i];
296
+ const content = String(message.content ?? '');
297
+ const role = message.role === 'assistant' ? 'donna' : message.role;
298
+ if (i < backed.length) {
299
+ const entry = backed[i];
300
+ if (entry.content !== content) entry.content = content;
301
+ if (entry.role !== role) entry.role = role;
302
+ continue;
303
+ }
304
+ if (role === 'user') {
305
+ // The user's own message was already pushed optimistically (see
306
+ // useAgent.ts) and marked _pending — confirm that entry instead of
307
+ // adding a second copy. No matching pending entry (e.g. history
308
+ // restored after a fresh reconnect) falls through to a normal push.
309
+ const pendingIndex = target.findIndex((entry: any) => entry._pending && entry.role === 'user' && entry.content === content);
310
+ if (pendingIndex !== -1) {
311
+ delete target[pendingIndex]._pending;
312
+ backed.push(target[pendingIndex]);
313
+ continue;
314
+ }
315
+ }
316
+ const entry = { role, content };
317
+ target.push(entry);
318
+ backed.push(entry);
319
+ }
320
+ }
321
+
266
322
  function syncRuntimeState() {
267
323
  void fetchRuntimeState({ url: props.runtime.url, workspace: (session as any).workspace ?? null })
268
324
  .then((state) => {
269
325
  setRuntimeState(state);
326
+ mergeRuntimeConversation(state);
270
327
  setRuntimeStatus('connected');
271
328
  refresh();
272
329
  })
@@ -310,6 +367,22 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
310
367
  syncRuntimeState();
311
368
  void subscribeRuntimeEvents();
312
369
  }
370
+
371
+ let currentRuntimeWorkspace = (session as any).workspace ?? null;
372
+ function resyncRuntimeWorkspaceIfChanged() {
373
+ const workspace = (session as any).workspace ?? null;
374
+ if (workspace === currentRuntimeWorkspace) return;
375
+ currentRuntimeWorkspace = workspace;
376
+ if (!props.runtime?.url) return;
377
+ if (runtimeReconnectTimer) {
378
+ clearTimeout(runtimeReconnectTimer);
379
+ runtimeReconnectTimer = null;
380
+ }
381
+ runtimeStreamAbort?.abort();
382
+ syncRuntimeState();
383
+ void subscribeRuntimeEvents();
384
+ }
385
+
313
386
  onCleanup(() => {
314
387
  runtimeStreamStopped = true;
315
388
  runtimeStreamAbort?.abort();
@@ -408,6 +481,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
408
481
  lastActivityLines.clear();
409
482
  lastActivityDetails.clear();
410
483
  const result = await agent.submit(line);
484
+ resyncRuntimeWorkspaceIfChanged();
411
485
  if ((result as any)?.setMode === 'chat') {
412
486
  setChatMode(true);
413
487
  (session as any).chatMode = true;
@@ -523,6 +597,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
523
597
  setInput: updateInput,
524
598
  title,
525
599
  statusLine,
600
+ runtimeHint,
526
601
  chatMode,
527
602
  showWelcome,
528
603
  activeEditor,