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.
- package/dist/app/app.js +8 -6
- package/dist/app/constants.d.ts +1 -0
- package/dist/app/constants.js +2 -1
- package/dist/app/input/voice-controller.js +16 -12
- package/dist/app/popup/popup-menu-controller.d.ts +1 -5
- package/dist/app/popup/popup-menu-controller.js +7 -8
- package/dist/app/process.js +7 -0
- package/dist/app/rendering/conversation-entry-renderer.js +17 -16
- package/dist/app/rendering/conversation-viewport.js +4 -35
- package/dist/app/rendering/editor-layout-renderer.d.ts +5 -1
- package/dist/app/rendering/editor-layout-renderer.js +29 -20
- package/dist/app/rendering/popup-menu-renderer.d.ts +1 -5
- package/dist/app/rendering/popup-menu-renderer.js +24 -34
- package/dist/app/rendering/render-controller.d.ts +2 -0
- package/dist/app/rendering/render-controller.js +40 -49
- package/dist/app/rendering/render-text.js +2 -2
- package/dist/app/rendering/status-line-renderer.d.ts +2 -0
- package/dist/app/rendering/status-line-renderer.js +75 -29
- package/dist/app/rendering/tab-line-renderer.js +3 -3
- package/dist/app/runtime.js +29 -3
- package/dist/app/screen/file-link-opener.d.ts +2 -0
- package/dist/app/screen/file-link-opener.js +84 -17
- package/dist/app/screen/mouse-controller.d.ts +1 -3
- package/dist/app/screen/mouse-controller.js +14 -14
- package/dist/app/screen/screen-styler.js +1 -1
- package/dist/app/session/lazy-session-manager.d.ts +1 -1
- package/dist/app/session/lazy-session-manager.js +64 -52
- package/dist/app/session/queued-message-controller.d.ts +6 -0
- package/dist/app/session/queued-message-controller.js +9 -1
- package/dist/app/session/queued-message-entries.d.ts +8 -0
- package/dist/app/session/queued-message-entries.js +41 -0
- package/dist/app/session/session-lifecycle-controller.d.ts +9 -1
- package/dist/app/session/session-lifecycle-controller.js +45 -11
- package/dist/app/session/tabs-controller.d.ts +11 -1
- package/dist/app/session/tabs-controller.js +197 -30
- package/dist/app/terminal/terminal-controller.d.ts +2 -0
- package/dist/app/terminal/terminal-controller.js +7 -5
- package/dist/app/types.d.ts +1 -1
- package/dist/schemas/pi-tools-suite-schema.d.ts +3 -0
- package/dist/schemas/pi-tools-suite-schema.js +3 -0
- package/dist/theme.d.ts +11 -0
- package/dist/theme.js +26 -4
- package/extensions/question/tui.ts +1 -1
- package/extensions/session-title/config.ts +3 -3
- package/extensions/session-title/index.ts +60 -5
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +1 -0
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -3
- package/external/pi-tools-suite/src/async-subagents/core/notifications.ts +64 -0
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -0
- package/external/pi-tools-suite/src/async-subagents/index.ts +54 -8
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -4
- package/external/pi-tools-suite/src/config.ts +13 -0
- package/external/pi-tools-suite/src/dcp/state.ts +9 -4
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +5 -1
- package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +683 -0
- package/external/pi-tools-suite/src/index.ts +1 -0
- package/external/pi-tools-suite/src/lib/lsp.ts +2 -5
- package/external/pi-tools-suite/src/lsp/_shared/config.ts +2 -0
- package/external/pi-tools-suite/src/lsp/_shared/types.ts +2 -0
- package/external/pi-tools-suite/src/lsp/manager.ts +15 -9
- package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +1 -0
- package/external/pi-tools-suite/src/todo/index.ts +81 -4
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +5 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
- package/package.json +3 -14
- package/schemas/pi-tools-suite.json +19 -0
- package/apps/desktop-tauri/README.md +0 -103
- 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
|
|
40
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
}
|
|
161
|
-
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (match.server.waitForPublishDiagnostics === false || diagnosticsWaitMs <= 0) {
|
|
167
|
+
continue;
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
const entry = await this.diagnostics.waitForFile(
|
|
@@ -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 =
|
|
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("
|
|
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 (
|
|
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.
|
|
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.
|
|
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
|
-
}
|