@geminilight/mindos 0.5.19 → 0.5.21

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 (39) hide show
  1. package/app/app/api/ask/route.ts +308 -172
  2. package/app/app/api/file/route.ts +35 -11
  3. package/app/app/api/skills/route.ts +22 -3
  4. package/app/components/SettingsModal.tsx +52 -58
  5. package/app/components/Sidebar.tsx +21 -1
  6. package/app/components/settings/AiTab.tsx +4 -25
  7. package/app/components/settings/AppearanceTab.tsx +31 -13
  8. package/app/components/settings/KnowledgeTab.tsx +13 -28
  9. package/app/components/settings/McpAgentInstall.tsx +227 -0
  10. package/app/components/settings/McpServerStatus.tsx +172 -0
  11. package/app/components/settings/McpSkillsSection.tsx +583 -0
  12. package/app/components/settings/McpTab.tsx +16 -728
  13. package/app/components/settings/PluginsTab.tsx +4 -27
  14. package/app/components/settings/Primitives.tsx +69 -0
  15. package/app/components/settings/ShortcutsTab.tsx +2 -4
  16. package/app/components/settings/SyncTab.tsx +8 -24
  17. package/app/components/settings/types.ts +116 -2
  18. package/app/lib/agent/context.ts +151 -87
  19. package/app/lib/agent/index.ts +4 -3
  20. package/app/lib/agent/model.ts +76 -10
  21. package/app/lib/agent/stream-consumer.ts +73 -77
  22. package/app/lib/agent/to-agent-messages.ts +106 -0
  23. package/app/lib/agent/tools.ts +260 -266
  24. package/app/lib/i18n-en.ts +480 -0
  25. package/app/lib/i18n-zh.ts +505 -0
  26. package/app/lib/i18n.ts +4 -947
  27. package/app/next-env.d.ts +1 -1
  28. package/app/next.config.ts +7 -0
  29. package/app/package-lock.json +3258 -3093
  30. package/app/package.json +6 -3
  31. package/bin/cli.js +140 -5
  32. package/package.json +4 -1
  33. package/scripts/setup.js +13 -0
  34. package/skills/mindos/SKILL.md +10 -168
  35. package/skills/mindos-zh/SKILL.md +14 -172
  36. package/templates/skill-rules/en/skill-rules.md +222 -0
  37. package/templates/skill-rules/en/user-rules.md +20 -0
  38. package/templates/skill-rules/zh/skill-rules.md +222 -0
  39. package/templates/skill-rules/zh/user-rules.md +20 -0
@@ -1,9 +1,21 @@
1
- import type { Message, MessagePart, ToolCallPart, TextPart, ReasoningPart } from '@/lib/types';
2
-
3
1
  /**
4
- * Parse a UIMessageStream SSE response into structured Message parts.
5
- * The stream format is Server-Sent Events where each data line is a JSON-encoded UIMessageChunk.
2
+ * Parse MindOS SSE stream (6 event types) into structured Message parts.
3
+ *
4
+ * MindOS SSE format (backend: route.ts):
5
+ * - text_delta: { type, delta }
6
+ * - thinking_delta: { type, delta } (Anthropic extended thinking)
7
+ * - tool_start: { type, toolCallId, toolName, args }
8
+ * - tool_end: { type, toolCallId, output, isError }
9
+ * - done: { type, usage? }
10
+ * - error: { type, message }
11
+ *
12
+ * Frontend Message structure:
13
+ * - role: 'assistant'
14
+ * - content: concatenated text deltas (for display)
15
+ * - parts: structured [TextPart | ReasoningPart | ToolCallPart] (for detailed view)
6
16
  */
17
+ import type { Message, MessagePart, ToolCallPart, TextPart, ReasoningPart } from '@/lib/types';
18
+
7
19
  export async function consumeUIMessageStream(
8
20
  body: ReadableStream<Uint8Array>,
9
21
  onUpdate: (message: Message) => void,
@@ -13,18 +25,19 @@ export async function consumeUIMessageStream(
13
25
  const decoder = new TextDecoder();
14
26
  let buffer = '';
15
27
 
16
- // Mutable working copies — we deep-clone when emitting to React
28
+ // Mutable working copies
17
29
  const parts: MessagePart[] = [];
18
30
  const toolCalls = new Map<string, ToolCallPart>();
19
31
  let currentTextId: string | null = null;
20
32
  let currentReasoningPart: ReasoningPart | null = null;
21
33
 
22
- /** Deep-clone parts into an immutable Message snapshot for React state */
34
+ /** Build an immutable Message snapshot from current parts */
23
35
  function buildMessage(): Message {
24
36
  const clonedParts: MessagePart[] = parts.map(p => {
25
37
  if (p.type === 'text') return { type: 'text' as const, text: p.text };
26
38
  if (p.type === 'reasoning') return { type: 'reasoning' as const, text: p.text };
27
- return { ...p }; // ToolCallPart — shallow copy is safe (all primitive fields + `input` is replaced, not mutated)
39
+ // ToolCallPart — shallow copy safe (primitive fields, input is replaced not mutated)
40
+ return { ...p };
28
41
  });
29
42
  const textContent = clonedParts
30
43
  .filter((p): p is TextPart => p.type === 'text')
@@ -37,6 +50,7 @@ export async function consumeUIMessageStream(
37
50
  };
38
51
  }
39
52
 
53
+ /** Get or create the last text part with given ID */
40
54
  function findOrCreateTextPart(id: string): TextPart {
41
55
  if (currentTextId === id) {
42
56
  const last = parts[parts.length - 1];
@@ -48,6 +62,7 @@ export async function consumeUIMessageStream(
48
62
  return part;
49
63
  }
50
64
 
65
+ /** Get or create a tool call part */
51
66
  function findOrCreateToolCall(toolCallId: string, toolName?: string): ToolCallPart {
52
67
  let tc = toolCalls.get(toolCallId);
53
68
  if (!tc) {
@@ -60,7 +75,7 @@ export async function consumeUIMessageStream(
60
75
  };
61
76
  toolCalls.set(toolCallId, tc);
62
77
  parts.push(tc);
63
- currentTextId = null; // break text continuity
78
+ currentTextId = null;
64
79
  }
65
80
  return tc;
66
81
  }
@@ -82,112 +97,93 @@ export async function consumeUIMessageStream(
82
97
  for (const line of lines) {
83
98
  const trimmed = line.trim();
84
99
 
85
- // SSE format: the ai SDK v6 UIMessageStream uses "d:{json}\n"
86
- // Also handle standard "data:{json}" for robustness
100
+ // Standard SSE format: "data:{json}"
87
101
  let jsonStr: string | null = null;
88
- if (trimmed.startsWith('d:')) {
89
- jsonStr = trimmed.slice(2);
90
- } else if (trimmed.startsWith('data:')) {
102
+ if (trimmed.startsWith('data:')) {
91
103
  jsonStr = trimmed.slice(5).trim();
92
104
  }
93
105
 
94
106
  if (!jsonStr) continue;
95
107
 
96
- let chunk: Record<string, unknown>;
108
+ let event: Record<string, unknown>;
97
109
  try {
98
- chunk = JSON.parse(jsonStr);
110
+ event = JSON.parse(jsonStr);
99
111
  } catch {
100
- continue; // skip malformed lines
112
+ continue; // skip malformed
101
113
  }
102
114
 
103
- const type = chunk.type as string;
115
+ const type = event.type as string;
104
116
 
105
117
  switch (type) {
106
- case 'text-start': {
107
- findOrCreateTextPart(chunk.id as string);
118
+ case 'text_delta': {
119
+ // Regular text from assistant
120
+ const part = findOrCreateTextPart('text');
121
+ part.text += (event.delta as string) ?? '';
108
122
  changed = true;
109
123
  break;
110
124
  }
111
- case 'text-delta': {
112
- const part = findOrCreateTextPart(chunk.id as string);
113
- part.text += chunk.delta as string;
114
- changed = true;
115
- break;
116
- }
117
- case 'text-end': {
118
- // Text part is complete — no state change needed
119
- break;
120
- }
121
- case 'tool-input-start': {
122
- const tc = findOrCreateToolCall(chunk.toolCallId as string, chunk.toolName as string);
123
- tc.state = 'running';
125
+
126
+ case 'thinking_delta': {
127
+ // Extended thinking (Anthropic)
128
+ if (!currentReasoningPart) {
129
+ currentReasoningPart = { type: 'reasoning', text: '' };
130
+ parts.push(currentReasoningPart);
131
+ currentTextId = null;
132
+ }
133
+ currentReasoningPart.text += (event.delta as string) ?? '';
124
134
  changed = true;
125
135
  break;
126
136
  }
127
- case 'tool-input-delta': {
128
- // Streaming input — we wait for input-available for the complete input
129
- break;
130
- }
131
- case 'tool-input-available': {
132
- const tc = findOrCreateToolCall(chunk.toolCallId as string, chunk.toolName as string);
133
- tc.input = chunk.input;
137
+
138
+ case 'tool_start': {
139
+ // Beginning of tool execution
140
+ const toolCallId = event.toolCallId as string;
141
+ const toolName = event.toolName as string;
142
+ const tc = findOrCreateToolCall(toolCallId, toolName);
143
+ tc.input = event.args;
134
144
  tc.state = 'running';
135
145
  changed = true;
136
146
  break;
137
147
  }
138
- case 'tool-output-available': {
139
- const tc = toolCalls.get(chunk.toolCallId as string);
140
- if (tc) {
141
- tc.output = chunk.output != null
142
- ? (typeof chunk.output === 'string' ? chunk.output : JSON.stringify(chunk.output))
143
- : '';
144
- tc.state = 'done';
145
- changed = true;
146
- }
147
- break;
148
- }
149
- case 'tool-output-error':
150
- case 'tool-input-error': {
151
- const tc = toolCalls.get(chunk.toolCallId as string);
148
+
149
+ case 'tool_end': {
150
+ // Tool execution finished
151
+ const toolCallId = event.toolCallId as string;
152
+ const tc = toolCalls.get(toolCallId);
152
153
  if (tc) {
153
- tc.output = (chunk.errorText as string) ?? (chunk.error as string) ?? 'Tool error';
154
- tc.state = 'error';
154
+ const output = event.output as string;
155
+ tc.output = output ?? '';
156
+ tc.state = (event.isError ? 'error' : 'done');
155
157
  changed = true;
156
158
  }
157
159
  break;
158
160
  }
161
+
159
162
  case 'error': {
160
- const errorText = (chunk.errorText as string) ?? 'Unknown error';
161
- parts.push({ type: 'text', text: `\n\n**Error:** ${errorText}` });
162
- currentTextId = null;
163
- changed = true;
164
- break;
165
- }
166
- // step-start, metadata, finish — ignored for now
167
- case 'reasoning-start': {
168
- currentReasoningPart = { type: 'reasoning', text: '' };
169
- parts.push(currentReasoningPart);
163
+ // Stream error
164
+ const message = event.message as string;
165
+ parts.push({
166
+ type: 'text',
167
+ text: `\n\n**Stream Error:** ${message}`,
168
+ });
170
169
  currentTextId = null;
171
170
  changed = true;
172
171
  break;
173
172
  }
174
- case 'reasoning-delta': {
175
- if (currentReasoningPart) {
176
- currentReasoningPart.text += chunk.delta as string;
177
- changed = true;
178
- }
179
- break;
180
- }
181
- case 'reasoning-end': {
182
- currentReasoningPart = null;
173
+
174
+ case 'done': {
175
+ // Stream completed cleanly — usage data is optional
176
+ // No state change needed; just marks end of SSE stream
183
177
  break;
184
178
  }
179
+
185
180
  default:
181
+ // Ignore unknown event types
186
182
  break;
187
183
  }
188
184
  }
189
185
 
190
- // Emit once per reader.read() batch, not per SSE line
186
+ // Emit once per reader batch, not per SSE line
191
187
  if (changed) {
192
188
  onUpdate(buildMessage());
193
189
  }
@@ -196,8 +192,8 @@ export async function consumeUIMessageStream(
196
192
  reader.releaseLock();
197
193
  }
198
194
 
199
- // Finalize any tool calls still stuck in running/pending state
200
- // (stream ended before their output arrived e.g. abort, network error, step limit)
195
+ // Finalize any tool calls still in running/pending state
196
+ // (stream ended unexpectedly — abort, network error, step limit)
201
197
  let finalized = false;
202
198
  for (const tc of toolCalls.values()) {
203
199
  if (tc.state === 'running' || tc.state === 'pending') {
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Convert frontend Message[] (with parts containing tool calls + results)
3
+ * into pi-agent-core AgentMessage[] that Agent expects.
4
+ *
5
+ * This is "Layer 1" of the two-layer conversion:
6
+ * Frontend Message[] → AgentMessage[] (this file)
7
+ * AgentMessage[] → pi-ai Message[] (handled by Agent's convertToLlm internally)
8
+ *
9
+ * Key responsibilities:
10
+ * - User messages: wrap as { role: 'user', content, timestamp }
11
+ * - Assistant messages: convert parts into { role: 'assistant', content: [...] }
12
+ * - Tool results: emit separate { role: 'toolResult', ... } per tool call
13
+ * - Orphaned tool calls (running/pending from interrupted streams): supply empty result
14
+ * - Reasoning parts: filtered out (display-only, not sent back to LLM)
15
+ */
16
+ import type { Message as FrontendMessage, ToolCallPart as FrontendToolCallPart } from '@/lib/types';
17
+ import type { AgentMessage } from '@mariozechner/pi-agent-core';
18
+ import type { UserMessage, AssistantMessage, ToolResultMessage } from '@mariozechner/pi-ai';
19
+
20
+ // Re-export for convenience
21
+ export type { AgentMessage } from '@mariozechner/pi-agent-core';
22
+
23
+ export function toAgentMessages(messages: FrontendMessage[]): AgentMessage[] {
24
+ const result: AgentMessage[] = [];
25
+
26
+ for (const msg of messages) {
27
+ if (msg.role === 'user') {
28
+ result.push({
29
+ role: 'user',
30
+ content: msg.content,
31
+ timestamp: Date.now(),
32
+ } satisfies UserMessage as AgentMessage);
33
+ continue;
34
+ }
35
+
36
+ // Skip error placeholder messages from frontend
37
+ if (msg.content.startsWith('__error__')) continue;
38
+
39
+ // Assistant message
40
+ if (!msg.parts || msg.parts.length === 0) {
41
+ // Plain text assistant message — no tool calls
42
+ if (msg.content) {
43
+ result.push({
44
+ role: 'assistant',
45
+ content: [{ type: 'text', text: msg.content }],
46
+ // Minimal required fields for historical messages
47
+ api: 'anthropic-messages' as any,
48
+ provider: 'anthropic' as any,
49
+ model: '',
50
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
51
+ stopReason: 'stop',
52
+ timestamp: Date.now(),
53
+ } satisfies AssistantMessage as AgentMessage);
54
+ }
55
+ continue;
56
+ }
57
+
58
+ // Build assistant content array (text + tool calls)
59
+ const assistantContent: AssistantMessage['content'] = [];
60
+ const toolCalls: FrontendToolCallPart[] = [];
61
+
62
+ for (const part of msg.parts) {
63
+ if (part.type === 'text') {
64
+ if (part.text) {
65
+ assistantContent.push({ type: 'text', text: part.text });
66
+ }
67
+ } else if (part.type === 'tool-call') {
68
+ assistantContent.push({
69
+ type: 'toolCall' as any,
70
+ id: part.toolCallId,
71
+ name: part.toolName,
72
+ arguments: part.input ?? {},
73
+ });
74
+ toolCalls.push(part);
75
+ }
76
+ // 'reasoning' parts are display-only; not sent back to model
77
+ }
78
+
79
+ if (assistantContent.length > 0) {
80
+ result.push({
81
+ role: 'assistant',
82
+ content: assistantContent,
83
+ api: 'anthropic-messages' as any,
84
+ provider: 'anthropic' as any,
85
+ model: '',
86
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
87
+ stopReason: toolCalls.length > 0 ? 'toolUse' : 'stop',
88
+ timestamp: Date.now(),
89
+ } satisfies AssistantMessage as AgentMessage);
90
+ }
91
+
92
+ // Emit tool result messages for each tool call
93
+ for (const tc of toolCalls) {
94
+ result.push({
95
+ role: 'toolResult',
96
+ toolCallId: tc.toolCallId,
97
+ toolName: tc.toolName,
98
+ content: [{ type: 'text', text: tc.output ?? '' }],
99
+ isError: tc.state === 'error',
100
+ timestamp: Date.now(),
101
+ } satisfies ToolResultMessage as AgentMessage);
102
+ }
103
+ }
104
+
105
+ return result;
106
+ }