pi-ui-extend 0.1.18 → 0.1.20

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 (70) hide show
  1. package/dist/app/app.js +8 -6
  2. package/dist/app/constants.d.ts +1 -0
  3. package/dist/app/constants.js +2 -1
  4. package/dist/app/input/voice-controller.js +16 -12
  5. package/dist/app/popup/popup-menu-controller.d.ts +1 -5
  6. package/dist/app/popup/popup-menu-controller.js +7 -8
  7. package/dist/app/process.js +7 -0
  8. package/dist/app/rendering/conversation-entry-renderer.js +17 -16
  9. package/dist/app/rendering/conversation-viewport.js +4 -35
  10. package/dist/app/rendering/editor-layout-renderer.d.ts +5 -1
  11. package/dist/app/rendering/editor-layout-renderer.js +29 -20
  12. package/dist/app/rendering/popup-menu-renderer.d.ts +1 -5
  13. package/dist/app/rendering/popup-menu-renderer.js +24 -34
  14. package/dist/app/rendering/render-controller.d.ts +2 -0
  15. package/dist/app/rendering/render-controller.js +40 -49
  16. package/dist/app/rendering/render-text.js +2 -2
  17. package/dist/app/rendering/status-line-renderer.d.ts +2 -0
  18. package/dist/app/rendering/status-line-renderer.js +75 -29
  19. package/dist/app/rendering/tab-line-renderer.js +3 -3
  20. package/dist/app/runtime.js +29 -3
  21. package/dist/app/screen/file-link-opener.d.ts +2 -0
  22. package/dist/app/screen/file-link-opener.js +84 -17
  23. package/dist/app/screen/mouse-controller.d.ts +1 -3
  24. package/dist/app/screen/mouse-controller.js +14 -14
  25. package/dist/app/screen/screen-styler.js +1 -1
  26. package/dist/app/session/lazy-session-manager.d.ts +1 -1
  27. package/dist/app/session/lazy-session-manager.js +64 -52
  28. package/dist/app/session/queued-message-controller.d.ts +6 -0
  29. package/dist/app/session/queued-message-controller.js +9 -1
  30. package/dist/app/session/queued-message-entries.d.ts +8 -0
  31. package/dist/app/session/queued-message-entries.js +41 -0
  32. package/dist/app/session/session-lifecycle-controller.d.ts +9 -1
  33. package/dist/app/session/session-lifecycle-controller.js +45 -11
  34. package/dist/app/session/tabs-controller.d.ts +11 -1
  35. package/dist/app/session/tabs-controller.js +197 -30
  36. package/dist/app/terminal/terminal-controller.d.ts +2 -0
  37. package/dist/app/terminal/terminal-controller.js +7 -5
  38. package/dist/app/types.d.ts +1 -1
  39. package/dist/schemas/pi-tools-suite-schema.d.ts +3 -0
  40. package/dist/schemas/pi-tools-suite-schema.js +3 -0
  41. package/dist/theme.d.ts +11 -0
  42. package/dist/theme.js +26 -4
  43. package/extensions/question/tui.ts +1 -1
  44. package/extensions/session-title/config.ts +3 -3
  45. package/extensions/session-title/index.ts +60 -5
  46. package/external/pi-tools-suite/README.md +3 -2
  47. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +1 -0
  48. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
  49. package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -3
  50. package/external/pi-tools-suite/src/async-subagents/core/notifications.ts +64 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -0
  52. package/external/pi-tools-suite/src/async-subagents/index.ts +54 -8
  53. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -4
  54. package/external/pi-tools-suite/src/config.ts +13 -0
  55. package/external/pi-tools-suite/src/dcp/state.ts +9 -4
  56. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +5 -1
  57. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +683 -0
  58. package/external/pi-tools-suite/src/index.ts +1 -0
  59. package/external/pi-tools-suite/src/lib/lsp.ts +2 -5
  60. package/external/pi-tools-suite/src/lsp/_shared/config.ts +2 -0
  61. package/external/pi-tools-suite/src/lsp/_shared/types.ts +2 -0
  62. package/external/pi-tools-suite/src/lsp/manager.ts +15 -9
  63. package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +1 -0
  64. package/external/pi-tools-suite/src/todo/index.ts +81 -4
  65. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +5 -0
  66. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  67. package/package.json +3 -14
  68. package/schemas/pi-tools-suite.json +19 -0
  69. package/apps/desktop-tauri/README.md +0 -103
  70. package/apps/desktop-tauri/bin/pix-desktop.mjs +0 -89
@@ -36,11 +36,8 @@ export async function appendLspDiagnosticsToMutationResult<T extends LspEnrichab
36
36
  if (files.length === 0) return options.result;
37
37
 
38
38
  const manager = getGlobalLspManager();
39
- const summaries: string[] = [];
40
- for (const file of files) {
41
- const summary = await manager.updateDiagnosticsForFile(options.ctx, file);
42
- if (summary.trim()) summaries.push(summary);
43
- }
39
+ const summaries = (await Promise.all(files.map((file) => manager.updateDiagnosticsForFile(options.ctx, file))))
40
+ .filter((summary) => summary.trim());
44
41
 
45
42
  const summary = summaries.join("\n\n");
46
43
  if (!summary.trim()) return options.result;
@@ -76,6 +76,8 @@ function parseLspItems(parsed: unknown): LspServerConfig[] {
76
76
  languageIdByExtension: cleanStringRecord(object.languageIdByExtension),
77
77
  startupTimeoutMs: cleanNumber(object.startupTimeoutMs),
78
78
  diagnosticsWaitMs: cleanNumber(object.diagnosticsWaitMs),
79
+ pullDiagnostics: cleanBoolean(object.pullDiagnostics),
80
+ waitForPublishDiagnostics: cleanBoolean(object.waitForPublishDiagnostics),
79
81
  initializationOptions: object.initializationOptions,
80
82
  settings: object.settings,
81
83
  });
@@ -30,6 +30,8 @@ export interface LspServerConfig extends MatchableConfig {
30
30
  languageIdByExtension?: Record<string, string>;
31
31
  startupTimeoutMs?: number;
32
32
  diagnosticsWaitMs?: number;
33
+ pullDiagnostics?: boolean;
34
+ waitForPublishDiagnostics?: boolean;
33
35
  initializationOptions?: unknown;
34
36
  settings?: unknown;
35
37
  }
@@ -149,16 +149,22 @@ export class LspManager {
149
149
  }
150
150
 
151
151
  let pullDiagnosticsError: string | undefined;
152
- try {
153
- const pulledDiagnostics = await client.pullDiagnostics(file, diagnosticsWaitMs, ctx.signal);
154
- if (pulledDiagnostics !== undefined) {
155
- const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, pulledDiagnostics);
156
- this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
157
- lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
158
- continue;
152
+ if (match.server.pullDiagnostics !== false) {
153
+ try {
154
+ const pulledDiagnostics = await client.pullDiagnostics(file, diagnosticsWaitMs, ctx.signal);
155
+ if (pulledDiagnostics !== undefined) {
156
+ const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, pulledDiagnostics);
157
+ this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
158
+ lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
159
+ continue;
160
+ }
161
+ } catch (error) {
162
+ pullDiagnosticsError = (error as Error).message;
159
163
  }
160
- } catch (error) {
161
- pullDiagnosticsError = (error as Error).message;
164
+ }
165
+
166
+ if (match.server.waitForPublishDiagnostics === false || diagnosticsWaitMs <= 0) {
167
+ continue;
162
168
  }
163
169
 
164
170
  const entry = await this.diagnostics.waitForFile(
@@ -237,6 +237,7 @@ export class IpcSocket {
237
237
  this.closed = true;
238
238
  this.clearTimers();
239
239
  for (const pending of this.pendingRequests.values()) {
240
+ clearTimeout(pending.timer);
240
241
  pending.reject(new Error("socket closed"));
241
242
  }
242
243
  this.pendingRequests.clear();
@@ -4,12 +4,15 @@ import { autoClearCompletedTodos } from "./state/auto-clear.js";
4
4
  import { loadPersistedPlan, syncPersistedPlan } from "./state/persistence.js";
5
5
  import { replayFromBranch } from "./state/replay.js";
6
6
  import { ACTIVE_STATUSES, isTaskBlocked, selectVisibleTasks } from "./state/selectors.js";
7
+ import { applyTaskMutation } from "./state/state-reducer.js";
7
8
  import { getState, replaceState } from "./state/store.js";
8
9
  import { DEFAULT_PROMPT_GUIDELINES, DEFAULT_PROMPT_SNIPPET, publishTodoState, registerTodosCommand, registerTodoTool } from "./todo.js";
9
- import type { TaskMutationParams } from "./tool/types.js";
10
+ import type { Task, TaskMutationParams } from "./tool/types.js";
11
+
12
+ type AgentMessageLike = { role?: unknown; stopReason?: unknown; content?: unknown };
10
13
 
11
14
  const TODO_NUDGE_LIMIT = 8;
12
- const TODO_NUDGE_INITIAL_DELAY_MS = 5_000;
15
+ const TODO_NUDGE_INITIAL_DELAY_MS = 0;
13
16
  const TODO_NUDGE_IDLE_RETRY_DELAY_MS = 100;
14
17
  const TODO_NUDGE_MAX_IDLE_ATTEMPTS = 40;
15
18
  const ASK_USER_TOOL_NAMES = new Set(["ask_user", "ask_user_question", "question"]);
@@ -119,6 +122,7 @@ export default function (pi: ExtensionAPI) {
119
122
  let nudgeTimer: ReturnType<typeof setTimeout> | undefined;
120
123
  const pendingAskUserToolCallIds = new Set<string>();
121
124
  let suppressNextNudgeForThinkingSwitch = false;
125
+ let inProgressAtAgentStart = new Set<number>();
122
126
 
123
127
  function registerTodoToolWithCurrentPrompt(): void {
124
128
  const thinkingPrompt = todoThinkingEnabled ? buildThinkingPromptParts(currentModel) : {};
@@ -193,6 +197,60 @@ export default function (pi: ExtensionAPI) {
193
197
  setter.call(pi, level);
194
198
  }
195
199
 
200
+ function isCompletedAssistantReply(message: AgentMessageLike | undefined): boolean {
201
+ if (message?.role !== "assistant") return false;
202
+ if (message.stopReason === "aborted" || message.stopReason === "error" || message.stopReason === "length") return false;
203
+ if (!Array.isArray(message.content)) return false;
204
+ return message.content.some((block) => typeof (block as { type?: unknown }).type === "string");
205
+ }
206
+
207
+ function hasCompletedAssistantReply(messages: readonly unknown[] | undefined): boolean {
208
+ if (!Array.isArray(messages)) return false;
209
+ for (let i = messages.length - 1; i >= 0; i--) {
210
+ const message = messages[i] as AgentMessageLike | undefined;
211
+ if (message?.role !== "assistant") continue;
212
+ return isCompletedAssistantReply(message);
213
+ }
214
+ return false;
215
+ }
216
+
217
+ function findOptimisticallyCompletableTask(tasks: readonly Task[]): Task | undefined {
218
+ const visible = tasks.filter((task) => task.status !== "deleted");
219
+ const byId = new Map(visible.map((task) => [task.id, task]));
220
+ const unfinished = visible.filter((task) => ACTIVE_STATUSES.has(task.status) && !isTaskBlocked(task, byId));
221
+ if (unfinished.length !== 1) return undefined;
222
+ const [task] = unfinished;
223
+ if (task.status !== "in_progress") return undefined;
224
+ if (inProgressAtAgentStart.has(task.id)) return undefined;
225
+ return task;
226
+ }
227
+
228
+ function applyInternalTodoMutation(action: "update", params: TaskMutationParams, ctx: ExtensionContext): boolean {
229
+ const result = applyTaskMutation(getState(), action, params);
230
+ if (result.op.kind === "error") {
231
+ console.warn(`rpiv-todo: failed internal ${action} mutation — ${result.op.message}`);
232
+ return false;
233
+ }
234
+ const autoClear = autoClearCompletedTodos(result.state);
235
+ replaceState(autoClear.state);
236
+ publishTodoState(pi as any, ctx, action, params as Record<string, unknown>);
237
+ if (todoThinkingEnabled) applyTodoThinkingAfterCommit(autoClear.state, { action, params });
238
+ try {
239
+ const sync = syncPersistedPlan(ctx.cwd, autoClear.state);
240
+ if (sync?.completed) console.log(`rpiv-todo: completed persisted plan and removed ${sync.path}`);
241
+ } catch (err) {
242
+ console.warn(`rpiv-todo: failed to sync persisted plan — ${(err as Error).message}`);
243
+ }
244
+ return true;
245
+ }
246
+
247
+ function maybeRecoverCompletedCurrentTask(messages: readonly unknown[] | undefined, ctx: ExtensionContext): boolean {
248
+ if (!hasCompletedAssistantReply(messages)) return false;
249
+ const task = findOptimisticallyCompletableTask(getState().tasks);
250
+ if (!task) return false;
251
+ return applyInternalTodoMutation("update", { action: "update", id: task.id, status: "completed" }, ctx);
252
+ }
253
+
196
254
  function clearNudgeTimer(): void {
197
255
  if (!nudgeTimer) return;
198
256
  clearTimeout(nudgeTimer);
@@ -288,16 +346,35 @@ export default function (pi: ExtensionAPI) {
288
346
 
289
347
  pi.on("agent_start", async () => {
290
348
  pendingAskUserToolCallIds.clear();
349
+ inProgressAtAgentStart = new Set(selectVisibleTasks(getState()).filter((task) => task.status === "in_progress").map((task) => task.id));
291
350
  });
292
351
 
293
- pi.on("agent_end", async (_event, ctx) => {
352
+ pi.on("message_end", async (event, ctx) => {
353
+ if (!isCompletedAssistantReply((event as { message?: AgentMessageLike } | undefined)?.message)) return;
354
+ if (maybeRecoverCompletedCurrentTask([(event as { message?: AgentMessageLike }).message], ctx)) {
355
+ lastNudgedSignature = undefined;
356
+ clearNudgeTimer();
357
+ }
358
+ });
359
+
360
+ pi.on("agent_end", async (event, ctx) => {
361
+ const completedAssistantReply = hasCompletedAssistantReply((event as { messages?: readonly unknown[] } | undefined)?.messages);
362
+
294
363
  if (suppressNextNudgeForThinkingSwitch) {
295
364
  suppressNextNudgeForThinkingSwitch = false;
365
+ if (!completedAssistantReply) {
366
+ clearNudgeTimer();
367
+ return;
368
+ }
369
+ }
370
+
371
+ if (pendingAskUserToolCallIds.size > 0) {
296
372
  clearNudgeTimer();
297
373
  return;
298
374
  }
299
375
 
300
- if (pendingAskUserToolCallIds.size > 0) {
376
+ if (completedAssistantReply && maybeRecoverCompletedCurrentTask((event as { messages?: readonly unknown[] } | undefined)?.messages, ctx)) {
377
+ lastNudgedSignature = undefined;
301
378
  clearNudgeTimer();
302
379
  return;
303
380
  }
@@ -165,5 +165,10 @@ function appendWorkflowReminder(text: string, op: Op, state: TaskState): string
165
165
  "Reminder: pending todos exist but none is in_progress. Before starting work, call todo update on exactly one task with status in_progress and activeForm.",
166
166
  );
167
167
  }
168
+ if (hasInProgress) {
169
+ lines.push(
170
+ "Reminder: before you stop, update any finished todo items to completed. Treat the final user-facing report step like any other todo: once it is done, mark it completed immediately.",
171
+ );
172
+ }
168
173
  return lines.join("\n\n");
169
174
  }
@@ -77,7 +77,7 @@ export function asyncSubagentToolDescriptions(options: ToolDescriptionSetOptions
77
77
  repoDiscovery
78
78
  ? "Use for broad independent tracks, review axes, or hypotheses even though repo_* tools are available."
79
79
  : "Use first for broad codebase discovery split into tracks, review axes, or incident-triage hypotheses when repo_* tools are unavailable.",
80
- "Use action=spawn/status/wait/result/stop/cleanup with the matching options; spawned runs are registered under project .pi/subagents while the main session is alive so status/wait/stop can omit runDir for the latest run and result can resolve runDir by agentId.",
80
+ "Use action=spawn/status/wait/result/stop/cleanup with the matching options; spawned runs are registered under project .pi/subagents while the main session is alive so status/wait/stop can omit runDir for the latest run and result can resolve runDir by agentId. The parent session receives a follow-up system/custom message for each background agent when it finishes or fails, so the parent can stop after spawning instead of polling for completion.",
81
81
  "Collect compact results only when the parent task needs them.",
82
82
  "Spawned agents run in isolated background pi processes with extensions disabled to prevent recursive sub-agent spawning.",
83
83
  "Each agent has a wall-clock watchdog timeout (default 30 minutes); spawn timeoutSeconds or task timeoutSeconds can shorten it for tests or bounded probes.",
@@ -110,7 +110,7 @@ export function asyncSubagentToolDescriptions(options: ToolDescriptionSetOptions
110
110
  "For synthetic tests or intentionally bounded probes, pass timeoutSeconds slightly above the expected runtime so hung sub-agents are stopped automatically.",
111
111
  "For subagents action='spawn', default to leaving subagentType unset and let the lightweight router choose from configured role descriptions. Do not choose a role just because a built-in example seems to fit; the router has the current user config and presets. Set subagentType explicitly only when the user named the role, image inspection requires vision, tests need deterministic routing, or there is another concrete technical reason to bypass the router.",
112
112
  "Use subagentType='vision' with imagePaths and optional focus when a text-only/blind parent model needs a visual description of screenshots, UI state, diagrams, or other images.",
113
- "If the user asks to start, run, launch, or test parallel sub-agents, call action='spawn' and then stop; do not immediately call action='wait'.",
113
+ "If the user asks to start, run, launch, or test parallel sub-agents, call action='spawn' and then stop; completion/failure notifications will wake the parent so do not immediately call action='status' or action='wait' just to see whether agents finished.",
114
114
  "Use action='status' for a non-blocking progress check or to recover after reload/crash.",
115
115
  "After spawn, project-local .pi/subagents/registry.json records latest runDir and agentId mappings until normal main-session shutdown; if runDir is missing after compaction/reload, call status without runDir or result with agentId instead of failing solely because runDir was lost.",
116
116
  "Use action='wait' only when the user asks to wait/collect now, or your next parent step depends on completion.",
@@ -253,14 +253,14 @@ export const TODO_TOOL_DESCRIPTION: ToolDescription = {
253
253
  promptSnippet: "Track/sync non-trivial multi-step work; include final report item; resync when requirements change; keep one task in_progress",
254
254
  promptGuidelines: [
255
255
  "Use `todo` for complex work with 3+ steps, explicit user task lists, or new non-trivial requirements. Skip single trivial tasks and purely conversational requests.",
256
- "For any multi-step implementation/debugging plan, include a final todo item in the initial create/batch_create plan for the user-facing final report. Give that final-report todo explicit description/acceptance criteria covering changed files/behavior, verification commands/results, remaining manual actions, and never substitute a compression/housekeeping note for the final report.",
256
+ "For any multi-step implementation/debugging plan, include a final todo item in the initial create/batch_create plan for the user-facing final report. Treat it like any other todo item: as soon as that step is finished, mark it completed immediately. Give it explicit description/acceptance criteria covering changed files/behavior, verification commands/results, remaining manual actions, and never substitute a compression/housekeeping note for the final report.",
257
257
  "When the user adds, removes, cancels, reprioritizes, or changes the goal, scope, requirements, constraints, or chosen approach, use `todo` before continuing to synchronize the plan: update still-relevant tasks, defer or delete obsolete tasks, add new required tasks, and adjust dependencies/order.",
258
258
  "When your own investigation or verification discovers new facts that make the current todo plan stale, incomplete, impossible, unsafe, or no longer the best approach, use `todo` to revise the plan immediately instead of following outdated tasks.",
259
259
  "Update todos as part of the workflow, not as end-of-task cleanup: whenever you start, finish, block, split, abandon, or materially change a step, call `todo` immediately before continuing.",
260
260
  "Before any non-trivial read/edit/test/tool work on a planned task, mark exactly one task in_progress with activeForm (present-continuous label); do this immediately after creating a plan if no task is active. Mark it completed immediately after the required verification, never in batches.",
261
261
  "If implementation is partial, tests fail, or a blocker remains, keep the task in_progress and add/update a blocker task instead of completing it.",
262
262
  "Never use `clear`, `delete`, or batch deletion to hide unfinished, stale, or forgotten todos. Defer obsolete items or update them with the reason; only delete when the user explicitly asks or the item was created by mistake.",
263
- "Before giving a final response for work that used todos, ensure every visible todo is completed, deferred, or intentionally still in_progress with a blocker/explanation.",
263
+ "Before giving a final response for work that used todos, ensure every visible todo is completed, deferred, or intentionally still in_progress with a blocker/explanation. Do not leave any just-finished todo item in_progress after you stop.",
264
264
  "Keep subjects short and imperative; put details in description only when needed. Use parentId for large plans; use blockedBy on create and addBlockedBy/removeBlockedBy on update for dependencies.",
265
265
  "Use batch_create/batch_update for large explicit plans, but still keep exactly one visible task in_progress unless the user asks otherwise.",
266
266
  "When starting a new plan that supersedes existing unfinished todos, use batch_create with replace:true instead of appending; only omit replace when intentionally extending the current plan.",
package/package.json CHANGED
@@ -1,15 +1,10 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "private": false,
5
5
  "type": "module",
6
- "workspaces": [
7
- "apps/desktop-tauri",
8
- "apps/desktop-tauri/sidecar"
9
- ],
10
6
  "bin": {
11
- "pix": "./bin/pix.mjs",
12
- "pix-desktop": "./apps/desktop-tauri/bin/pix-desktop.mjs"
7
+ "pix": "./bin/pix.mjs"
13
8
  },
14
9
  "exports": {
15
10
  ".": {
@@ -63,13 +58,7 @@
63
58
  "generate-schemas": "tsx scripts/generate-schemas.ts",
64
59
  "generate-schemas:check": "tsx scripts/generate-schemas.ts --check",
65
60
  "generate-schemas:watch": "tsx watch scripts/generate-schemas.ts",
66
- "prepublishOnly": "npm run check && npm run build:pix && npm run generate-schemas",
67
- "desktop:install": "npm --prefix apps/desktop-tauri install",
68
- "desktop:dev": "npm --prefix apps/desktop-tauri run tauri:dev",
69
- "desktop:build": "npm --prefix apps/desktop-tauri run tauri:build",
70
- "desktop:check": "npm --prefix apps/desktop-tauri run build && npm --prefix apps/desktop-tauri/sidecar run check && cargo check --manifest-path apps/desktop-tauri/src-tauri/Cargo.toml",
71
- "desktop:icons": "npm --prefix apps/desktop-tauri run icons",
72
- "desktop": "node apps/desktop-tauri/bin/pix-desktop.mjs"
61
+ "prepublishOnly": "npm run check && npm run build:pix && npm run generate-schemas"
73
62
  },
74
63
  "dependencies": {
75
64
  "@earendil-works/pi-tui": "0.77.0",
@@ -20,6 +20,17 @@
20
20
  "type": "boolean",
21
21
  "description": "Enable per-todo thinking levels and automatic thinking switch/restore when tasks become in-progress/completed."
22
22
  },
23
+ "lookupModel": {
24
+ "anyOf": [
25
+ {
26
+ "type": "string"
27
+ },
28
+ {
29
+ "type": "null"
30
+ }
31
+ ],
32
+ "description": "Vision-capable provider/model used by GLM's lookup tool; unset or null disables lookup."
33
+ },
23
34
  "terminalBell": {
24
35
  "type": "object",
25
36
  "properties": {
@@ -862,6 +873,14 @@
862
873
  "description": "Wait time for diagnostics after file change.",
863
874
  "minimum": 0
864
875
  },
876
+ "pullDiagnostics": {
877
+ "type": "boolean",
878
+ "description": "When false, skip textDocument/diagnostic pull requests and rely on published diagnostics. Useful for servers where pull diagnostics is slow or incomplete."
879
+ },
880
+ "waitForPublishDiagnostics": {
881
+ "type": "boolean",
882
+ "description": "When false, do not wait for a fresh textDocument/publishDiagnostics notification after file change. diagnosticsWaitMs still bounds the wait when enabled."
883
+ },
865
884
  "initializationOptions": {
866
885
  "description": "LSP initialization options passed to the server."
867
886
  },
@@ -1,103 +0,0 @@
1
- # Pix Desktop (Tauri)
2
-
3
- Tauri-based desktop UI for the Pi coding agent. It is a sibling workspace of the `pix` terminal app and uses the same `@earendil-works/pi-coding-agent` SDK through a Node sidecar.
4
-
5
- > **Status — current prototype.** React talks to a Rust Tauri host via `rpc_call` / `rpc_subscribe`. Rust proxies line-delimited SDK-shaped JSON to a custom Node dispatcher. Implemented: workspace picker, persistent sessions, tabbed chat with per-workspace tab restore, expanded tool-call cards, history loading, SDK/pi-tools-suite slash-command discovery with extension argument completions, path/general composer autocomplete, image attachments via paste/drop/file picker, Web Speech voice dictation, captured `!shell` commands, raw `!!` PTY terminal surface, extension UI request dialogs with select search/timeouts, toasts/widgets/status plus session-scoped lifecycle, explicit degraded handling for custom/component extension UI, real pi-tools-suite interactive command smoke coverage, desktop-native `/model`/`/compact`/`/undo`, streaming/abort, and status bar. There is intentionally no sidebar.
6
-
7
- ## Architecture
8
-
9
- ```
10
- React (Vite, port 1420)
11
- │ invoke("rpc_call", { cmd }) / invoke("rpc_subscribe", { onEvent })
12
-
13
- Tauri Rust host (src-tauri/)
14
- │ JSONL over stdio, SDK flat shape { id?, type, ... }
15
-
16
- Node sidecar (sidecar/src/)
17
- │ custom dispatcher + @earendil-works/pi-coding-agent SDK
18
-
19
- AgentSession / SessionManager
20
- ```
21
-
22
- The frontend never imports or calls the SDK directly. Rust owns native APIs and sidecar process management; the sidecar owns SDK runtime/session operations.
23
-
24
- ## Layout
25
-
26
- ```
27
- apps/desktop-tauri/
28
- ├── src/ # React frontend
29
- │ ├── App.tsx # workspace picker, topbar, tabs, chat, history transform
30
- │ ├── App.css # Tokyo-Night dark theme
31
- │ └── tools/ # tool-call renderer registry and renderers
32
- ├── sidecar/src/ # Node SDK bridge
33
- │ ├── main.ts # create runtime, switch cwd, run dispatcher
34
- │ ├── dispatcher.ts # command switch and event subscription rebinding
35
- │ ├── pix-handlers.ts # pix:list_sessions
36
- │ ├── framing.ts # strict LF JSONL framing
37
- │ └── protocol.ts # wire types
38
- └── src-tauri/src/ # Rust host and sidecar bridge
39
- ```
40
-
41
- ## Wire protocol
42
-
43
- This is **not JSON-RPC 2.0**. The sidecar uses the SDK-style flat JSONL protocol:
44
-
45
- - Command: `{ "id": "req-1", "type": "prompt", "message": "hi", "images": [] }`
46
- - Response: `{ "id": "req-1", "type": "response", "command": "prompt", "success": true, "data": ... }`
47
- - Event: `{ "type": "agent_start" | "message_update" | "tool_execution_*" | ... }`
48
-
49
- Implemented sidecar commands include `prompt`, `abort`, `get_state`, `get_messages`, `get_session_stats`, `get_commands`, `get_command_completions`, `extension_ui_response`, `get_models`, `set_model`, `compact`, `undo_last_turn`, `new_session`, `switch_session`, `set_session_name`, `pix:list_sessions`, and `pix:set_cwd`. The sidecar emits `extension_ui_request` events for extension `ctx.ui.*` calls, and the frontend answers dialog methods with `extension_ui_response`. The Rust host also exposes native `run_shell`, `complete_path`, and `pty_*` commands for the desktop `!cmd` flow, composer path autocomplete, and raw `!!` terminal sessions.
50
-
51
- ## Setup and run
52
-
53
- From the repo root:
54
-
55
- ```bash
56
- npm install --ignore-scripts
57
- npm run desktop:icons
58
- npm run desktop:dev
59
- ```
60
-
61
- On first launch, choose a project folder. Pix Desktop stores the selected workspace in localStorage under `pix-desktop.workspace`, stores open tabs per cwd under `pix-desktop.tabs:<cwd>`, and resumes the saved active tab when possible.
62
-
63
- ## Verification
64
-
65
- ```bash
66
- npm run desktop:check
67
- ```
68
-
69
- Equivalent expanded checks:
70
-
71
- ```bash
72
- npm --prefix apps/desktop-tauri run build
73
- npm --prefix apps/desktop-tauri/sidecar run check
74
- cargo check --manifest-path apps/desktop-tauri/src-tauri/Cargo.toml
75
- ```
76
-
77
- ## Env overrides
78
-
79
- | Variable | Purpose |
80
- | --- | --- |
81
- | `PIX_SIDECAR_CMD` | Command to spawn, default `node`. |
82
- | `PIX_SIDECAR_ARGS` | Whitespace-separated args, overrides default `--import tsx`. |
83
- | `PIX_SIDECAR_PATH` | Explicit sidecar entry path. |
84
- | `PIX_SIDECAR_AGENT_DIR` | Pi agent dir for auth/skills/extensions, default `~/.pi/agent`. |
85
- | `PIX_SIDECAR_SESSION_MODE` | `persistent` by default; use `in-memory` for ephemeral tests. |
86
- | `RUST_LOG` | Rust tracing filter. |
87
-
88
- ## Current UX notes
89
-
90
- - Tabs are the only session navigation surface; the old sidebar was removed.
91
- - Open tabs and the active tab are restored per workspace across Tauri restarts.
92
- - Typing `/` opens a slash-command menu. Desktop built-ins (`/help`, `/new`, `/clear`, `/refresh`, `/abort`) are merged with SDK-discovered extension, prompt-template, and skill commands from `get_commands`; selecting a discovered command sends it through `prompt` with arguments preserved. When an extension command exposes `getArgumentCompletions`, the frontend debounces `get_command_completions` and shows argument suggestions in the same keyboard/click popup.
93
- - Path/general autocomplete is handled locally through the Rust `complete_path` helper, scoped to the selected workspace. It completes `@path` mentions in normal messages, path-like `!cmd` shell tokens, and generic slash-command arguments when no richer extension/model completion is available.
94
- - Images can be attached from the composer with the image button, paste, or drag/drop. The frontend previews them locally, sends SDK `ImageContent[]` through the sidecar `prompt` command, and keeps file attachments as text/path mentions for now.
95
- - Voice dictation is available from the composer mic button when the current WebView exposes `SpeechRecognition`/`webkitSpeechRecognition`; final transcripts are appended to the composer. Unsupported/error states are shown inline. Offline Vosk parity remains future work.
96
- - Extension commands can request simple UI through the RPC-style `extension_ui_request` surface: `select`, `confirm`, `input`, and `editor` show modal dialogs. Select dialogs include local search, long-list scrolling, option counts, and timeout countdown/self-cancel affordances when an extension passes `dialogOpts.timeout`; `notify` shows toasts; `setWidget` renders scroll-contained text widgets above/below the composer; `setStatus` and working-indicator APIs add status-bar entries; `setHeader`/`setFooter`, component widgets, `custom()`, `setEditorComponent()`, and extension autocomplete providers now have explicit degraded desktop behavior/status instead of silent no-ops; extension widgets/status/dialogs are cleared on session/workspace switch so stale UI does not leak across tabs; `set_editor_text` fills the composer.
97
- - Real `pi-tools-suite` command smoke checks have exercised `/prompt-commands` (`select`, `notify`, `input` cancelled before mutation) and `/subagent-preset-config` (`select` cancelled) through the desktop sidecar path. Keep `PIX_SIDECAR_SESSION_MODE=in-memory` for these checks, but do not redirect `HOME` unless extension discovery is configured for that home; otherwise user extensions are not loaded and slash commands fall through to normal prompts.
98
- - Desktop-native interactive built-ins are available for commands that are not prompt-invokable: `/model` opens a model picker and `/model <provider/id>` sets directly; `/compact [instructions]` runs SDK compaction; `/undo` navigates back to the latest user turn and restores returned editor text when available.
99
- - Typing `!command` runs a short non-interactive shell command in the selected workspace and renders captured stdout/stderr as a shell tool card. Typing `!!command` opens an xterm.js-backed PTY panel in the current workspace for interactive/raw terminal programs; bare `!!` opens the user's shell.
100
- - Tool-call cards include specialized renderers for shell/file/patch/todo/web/folder operations plus repo-index tools, ast-grep/apply, question prompts, subagent orchestration, context compression, and skill activation.
101
- - Switching tabs/folders and closing tabs are blocked while an agent run is streaming, because the sidecar has one active SDK session subscription.
102
- - On session switch or workspace restore, the UI calls `get_messages` and transforms SDK messages into sanitized chat messages, filtering reasoning/image internals and attaching tool results to tool-call cards.
103
- - Sidecar logs must go to stderr only; stdout is reserved for JSONL protocol records.
@@ -1,89 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * pix-desktop — CLI launcher for the Tauri-based Pix Desktop UI.
4
- *
5
- * Behaves like the regular `pix` CLI: launch from any project folder and
6
- * Pix Desktop treats that folder as the workspace cwd, scoping sessions to
7
- * it just like the terminal pix does.
8
- *
9
- * Usage:
10
- * cd /path/to/project
11
- * pix-desktop
12
- *
13
- * Environment:
14
- * PIX_DESKTOP_BIN Path to a specific Tauri binary (overrides discovery).
15
- * PIX_DESKTOP_DEBUG Set to "1" to log resolved binary/cwd before launch.
16
- *
17
- * Build the binary first via one of:
18
- * cd apps/desktop-tauri && npm run tauri:dev # development (with hot reload)
19
- * cd apps/desktop-tauri && npm run tauri:build # production bundle
20
- */
21
-
22
- import { spawn } from "node:child_process";
23
- import { existsSync } from "node:fs";
24
- import { dirname, join } from "node:path";
25
- import { fileURLToPath } from "node:url";
26
- import { platform } from "node:os";
27
-
28
- const here = dirname(fileURLToPath(import.meta.url));
29
- const tauriTarget = join(here, "..", "src-tauri", "target");
30
-
31
- const exeSuffix = platform() === "win32" ? ".exe" : "";
32
- const macBundle = join(
33
- tauriTarget,
34
- "release",
35
- "bundle",
36
- "macos",
37
- "Pix.app",
38
- "Contents",
39
- "MacOS",
40
- "pix-desktop",
41
- );
42
-
43
- // Discovery order: env override → debug → release raw → release macOS bundle.
44
- const candidates = [
45
- join(tauriTarget, "debug", `pix-desktop${exeSuffix}`),
46
- join(tauriTarget, "release", `pix-desktop${exeSuffix}`),
47
- macBundle,
48
- ];
49
-
50
- if (process.env.PIX_DESKTOP_BIN) candidates.unshift(process.env.PIX_DESKTOP_BIN);
51
-
52
- const bin = candidates.find((p) => existsSync(p));
53
-
54
- if (!bin) {
55
- console.error("pix-desktop: Tauri binary not found. Build it first:");
56
- console.error("");
57
- console.error(" cd apps/desktop-tauri && npm run tauri:dev # development");
58
- console.error(" cd apps/desktop-tauri && npm run tauri:build # production");
59
- console.error("");
60
- console.error("Or set PIX_DESKTOP_BIN=/path/to/pix-desktop to point at a custom binary.");
61
- process.exit(127);
62
- }
63
-
64
- if (process.env.PIX_DESKTOP_DEBUG) {
65
- console.error(`[pix-desktop] binary: ${bin}`);
66
- console.error(`[pix-desktop] cwd: ${process.cwd()}`);
67
- }
68
-
69
- const child = spawn(bin, process.argv.slice(2), {
70
- stdio: "inherit",
71
- cwd: process.cwd(),
72
- env: process.env,
73
- });
74
-
75
- child.on("exit", (code, signal) => {
76
- if (signal) {
77
- // Re-raise the signal in the launcher so wrapping shells see it.
78
- try { process.kill(process.pid, signal); } catch { /* ignore */ }
79
- process.exit(128 + 15); // SIGTERM-ish
80
- }
81
- process.exit(code ?? 0);
82
- });
83
-
84
- // Forward common signals to the child so Ctrl-C cleans up the Tauri window.
85
- for (const sig of ["SIGINT", "SIGTERM"]) {
86
- process.on(sig, () => {
87
- if (!child.killed) child.kill(sig);
88
- });
89
- }