pilotswarm-cli 0.1.13 → 0.1.15

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/README.md CHANGED
@@ -22,6 +22,9 @@ npx pilotswarm local --env .env --plugin ./plugin --worker ./worker-module.js
22
22
 
23
23
  `pilotswarm-cli` provides the shipped TUI. Your app customizes it with `plugin/plugin.json`, `plugin/agents/*.agent.md`, `plugin/skills/*/SKILL.md`, and optional worker-side tools.
24
24
 
25
+ Portal/runtime helpers that are intentionally shared with `pilotswarm-web`
26
+ are exported from `pilotswarm-cli/portal`.
27
+
25
28
  Common docs:
26
29
 
27
30
  - CLI apps: `https://github.com/affandar/PilotSwarm/blob/main/docs/cli/building-cli-apps.md`
package/bin/tui.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { parseCliIntoEnv } from "../src/bootstrap-env.js";
4
- import { startTuiApp } from "../src/index.js";
3
+ // Force the shipped TUI onto production React/Ink unless the caller
4
+ // explicitly opts into another environment for debugging.
5
+ process.env.NODE_ENV ??= "production";
5
6
 
7
+ const { parseCliIntoEnv } = await import("../src/bootstrap-env.js");
6
8
  const config = parseCliIntoEnv(process.argv.slice(2));
9
+ const { startTuiApp } = await import("../src/index.js");
7
10
  await startTuiApp(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pilotswarm-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Terminal UI for PilotSwarm.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "main": "./src/index.js",
11
11
  "exports": {
12
- ".": "./src/index.js"
12
+ ".": "./src/index.js",
13
+ "./portal": "./src/portal.js"
13
14
  },
14
15
  "scripts": {
15
16
  "build": "echo 'pilotswarm-cli: no build step (plain JS)'",
@@ -36,7 +37,7 @@
36
37
  },
37
38
  "dependencies": {
38
39
  "ink": "^6.8.0",
39
- "pilotswarm-sdk": "^0.1.12",
40
+ "pilotswarm-sdk": "^0.1.15",
40
41
  "pilotswarm-ui-core": "0.1.0",
41
42
  "pilotswarm-ui-react": "0.1.0",
42
43
  "react": "^19.2.4"
package/src/app.js CHANGED
@@ -231,6 +231,7 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
231
231
  const focus = controller.getState().ui.focusRegion;
232
232
  const modal = controller.getState().ui.modal;
233
233
  const inspectorTab = controller.getState().ui.inspectorTab;
234
+ const fullscreenPane = controller.getState().ui.fullscreenPane || null;
234
235
  const plainShortcut = isPlainShortcutKey(key);
235
236
  const matchesCtrlKey = (name, controlChar) => key.ctrl
236
237
  && (key.name === name || input === name || input === controlChar);
@@ -241,6 +242,7 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
241
242
  const isCtrlA = matchesCtrlKey("a", "\u0001");
242
243
  const isShiftN = input === "N" || (key.shift && key.name === "n");
243
244
  const isShiftD = input === "D" || (key.shift && key.name === "d");
245
+ const isShiftT = !key.ctrl && !key.meta && !key.alt && (input === "T" || (key.shift && key.name === "t"));
244
246
  const isAltBackspace = key.meta && (key.backspace || key.delete || key.name === "backspace" || key.name === "delete");
245
247
  const isAltLeftWord = key.meta && (key.leftArrow || key.name === "left" || input === "b" || input === "B");
246
248
  const isAltRightWord = key.meta && (key.rightArrow || key.name === "right" || input === "f" || input === "F");
@@ -275,6 +277,10 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
275
277
  }
276
278
 
277
279
  if (modal) {
280
+ if (isShiftT && modal.type === "themePicker") {
281
+ controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
282
+ return;
283
+ }
278
284
  if (modal.type === "renameSession" || modal.type === "artifactUpload") {
279
285
  if (key.escape) {
280
286
  controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
@@ -342,11 +348,19 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
342
348
  return;
343
349
  }
344
350
 
351
+ if (focus !== "prompt" && isShiftT) {
352
+ controller.handleCommand(UI_COMMANDS.OPEN_THEME_PICKER).catch(() => {});
353
+ return;
354
+ }
355
+
345
356
  if (key.tab && key.shift) {
346
357
  controller.handleCommand(UI_COMMANDS.FOCUS_PREV).catch(() => {});
347
358
  return;
348
359
  }
349
360
  if (key.tab) {
361
+ if (focus === "prompt" && controller.acceptPromptReferenceAutocomplete()) {
362
+ return;
363
+ }
350
364
  controller.handleCommand(UI_COMMANDS.FOCUS_NEXT).catch(() => {});
351
365
  return;
352
366
  }
@@ -354,6 +368,15 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
354
368
  controller.handleCommand(UI_COMMANDS.TOGGLE_FILE_PREVIEW_FULLSCREEN).catch(() => {});
355
369
  return;
356
370
  }
371
+ if (key.escape && fullscreenPane) {
372
+ if (focus === "prompt") {
373
+ controller.setPrompt("");
374
+ controller.setFocus(fullscreenPane);
375
+ return;
376
+ }
377
+ controller.handleCommand(UI_COMMANDS.TOGGLE_PANE_FULLSCREEN).catch(() => {});
378
+ return;
379
+ }
357
380
  if (key.escape) {
358
381
  if (focus === "prompt") {
359
382
  controller.setPrompt("");
@@ -387,10 +410,34 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
387
410
  controller.handleCommand(UI_COMMANDS.OPEN_FILES_FILTER).catch(() => {});
388
411
  return;
389
412
  }
413
+ if (focus === "inspector" && inspectorTab === "files" && input === "u") {
414
+ controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_UPLOAD).catch(() => {});
415
+ return;
416
+ }
417
+ if (focus === "inspector" && inspectorTab === "files" && input === "a") {
418
+ controller.handleCommand(UI_COMMANDS.DOWNLOAD_SELECTED_FILE).catch(() => {});
419
+ return;
420
+ }
421
+ if (focus === "inspector" && inspectorTab === "history" && input === "f") {
422
+ controller.handleCommand(UI_COMMANDS.OPEN_HISTORY_FORMAT).catch(() => {});
423
+ return;
424
+ }
425
+ if (focus === "inspector" && inspectorTab === "history" && input === "r") {
426
+ controller.handleCommand(UI_COMMANDS.REFRESH_EXECUTION_HISTORY).catch(() => {});
427
+ return;
428
+ }
429
+ if (focus === "inspector" && inspectorTab === "history" && input === "a") {
430
+ controller.handleCommand(UI_COMMANDS.EXPORT_EXECUTION_HISTORY).catch(() => {});
431
+ return;
432
+ }
390
433
  if (focus === "inspector" && inspectorTab === "files" && input === "v") {
391
434
  controller.handleCommand(UI_COMMANDS.TOGGLE_FILE_PREVIEW_FULLSCREEN).catch(() => {});
392
435
  return;
393
436
  }
437
+ if (focus !== "prompt" && input === "v") {
438
+ controller.handleCommand(UI_COMMANDS.TOGGLE_PANE_FULLSCREEN).catch(() => {});
439
+ return;
440
+ }
394
441
  if (focus === "inspector" && inspectorTab === "files" && plainShortcut && input === "o") {
395
442
  controller.handleCommand(UI_COMMANDS.OPEN_SELECTED_FILE).catch(() => {});
396
443
  return;
@@ -414,6 +461,14 @@ export function PilotSwarmTuiApp({ controller, platform, onRequestExit }) {
414
461
  controller.handleCommand(UI_COMMANDS.GROW_RIGHT_PANE).catch(() => {});
415
462
  return;
416
463
  }
464
+ if (focus !== "prompt" && input === "{") {
465
+ controller.handleCommand(UI_COMMANDS.SHRINK_SESSION_PANE).catch(() => {});
466
+ return;
467
+ }
468
+ if (focus !== "prompt" && input === "}") {
469
+ controller.handleCommand(UI_COMMANDS.GROW_SESSION_PANE).catch(() => {});
470
+ return;
471
+ }
417
472
 
418
473
  if (focus === "prompt") {
419
474
  if (isCtrlA) {
@@ -3,21 +3,10 @@ import { execFileSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { resolveTuiBranding } from "./plugin-config.js";
6
7
 
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
9
  const pkgRoot = path.resolve(__dirname, "..");
9
- const defaultTuiSplashPath = path.join(pkgRoot, "tui-splash.txt");
10
-
11
- function readPluginMetadata(pluginDir) {
12
- if (!pluginDir) return null;
13
- const pluginJsonPath = path.join(pluginDir, "plugin.json");
14
- if (!fs.existsSync(pluginJsonPath)) return null;
15
- try {
16
- return JSON.parse(fs.readFileSync(pluginJsonPath, "utf-8"));
17
- } catch (error) {
18
- throw new Error(`Failed to parse plugin metadata: ${pluginJsonPath}: ${error.message}`);
19
- }
20
- }
21
10
 
22
11
  function resolvePluginDir(flags) {
23
12
  if (flags.plugin) return path.resolve(flags.plugin);
@@ -52,31 +41,6 @@ function resolveSystemMessage(flags) {
52
41
  return undefined;
53
42
  }
54
43
 
55
- function resolveTuiBranding(pluginDir) {
56
- const pluginMeta = readPluginMetadata(pluginDir);
57
- const tui = pluginMeta?.tui;
58
- let defaultSplash = "{bold}{cyan-fg}PilotSwarm{/cyan-fg}{/bold}";
59
- if (fs.existsSync(defaultTuiSplashPath)) {
60
- defaultSplash = fs.readFileSync(defaultTuiSplashPath, "utf-8").trimEnd();
61
- }
62
- if (!tui || typeof tui !== "object") {
63
- return { title: "PilotSwarm", splash: defaultSplash };
64
- }
65
-
66
- const title = typeof tui.title === "string" && tui.title.trim() ? tui.title.trim() : "PilotSwarm";
67
- let splash = defaultSplash;
68
- if (typeof tui.splash === "string" && tui.splash.trim()) {
69
- splash = tui.splash;
70
- } else if (typeof tui.splashFile === "string" && tui.splashFile.trim()) {
71
- const splashPath = path.resolve(pluginDir, tui.splashFile);
72
- if (!fs.existsSync(splashPath)) {
73
- throw new Error(`TUI splash file not found: ${splashPath}`);
74
- }
75
- splash = fs.readFileSync(splashPath, "utf-8").trimEnd();
76
- }
77
- return { title, splash };
78
- }
79
-
80
44
  function loadEnvFile(envFile) {
81
45
  if (!fs.existsSync(envFile)) return;
82
46
  const envContent = fs.readFileSync(envFile, "utf-8");
package/src/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import React from "react";
2
2
  import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
3
5
  import { createRequire } from "node:module";
4
6
  import { render } from "ink";
5
7
  import {
@@ -14,6 +16,26 @@ import { NodeSdkTransport } from "./node-sdk-transport.js";
14
16
 
15
17
  const require = createRequire(import.meta.url);
16
18
 
19
+ const CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "pilotswarm");
20
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
21
+
22
+ function readConfig() {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+
30
+ function writeConfig(patch) {
31
+ try {
32
+ const existing = readConfig();
33
+ const merged = { ...existing, ...patch };
34
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
35
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
36
+ } catch {}
37
+ }
38
+
17
39
  function setupTuiHostRuntime() {
18
40
  const logFile = "/tmp/duroxide-tui.log";
19
41
  const originalConsole = {
@@ -90,9 +112,11 @@ export async function startTuiApp(config) {
90
112
  store: config.store,
91
113
  mode: config.mode,
92
114
  });
115
+ const userConfig = readConfig();
93
116
  const store = createStore(appReducer, createInitialState({
94
117
  mode: config.mode,
95
118
  branding: config.branding,
119
+ themeId: userConfig.themeId,
96
120
  }));
97
121
  const controller = new PilotSwarmUiController({ store, transport });
98
122
  let tuiApp;
@@ -144,6 +168,49 @@ export async function startTuiApp(config) {
144
168
  exitOnCtrlC: false,
145
169
  });
146
170
 
171
+ // Listen for portal theme-change OSC sequences on stdin:
172
+ // \x1b]777;theme;<themeId>\x07
173
+ let oscBuffer = "";
174
+ const OSC_PREFIX = "\x1b]777;theme;";
175
+ const OSC_SUFFIX = "\x07";
176
+ const stdinThemeHandler = (data) => {
177
+ const str = typeof data === "string" ? data : data.toString("utf8");
178
+ oscBuffer += str;
179
+ while (oscBuffer.includes(OSC_PREFIX) && oscBuffer.includes(OSC_SUFFIX)) {
180
+ const start = oscBuffer.indexOf(OSC_PREFIX);
181
+ const end = oscBuffer.indexOf(OSC_SUFFIX, start);
182
+ if (end < 0) break;
183
+ const themeId = oscBuffer.slice(start + OSC_PREFIX.length, end);
184
+ oscBuffer = oscBuffer.slice(end + OSC_SUFFIX.length);
185
+ if (themeId) {
186
+ store.dispatch({ type: "ui/theme", themeId });
187
+ }
188
+ }
189
+ // Prevent buffer from growing unbounded
190
+ if (oscBuffer.length > 1024) oscBuffer = oscBuffer.slice(-256);
191
+ };
192
+ process.stdin.on("data", stdinThemeHandler);
193
+
194
+ // Sync viewport on terminal resize (SIGWINCH)
195
+ const syncViewport = () => {
196
+ controller.setViewport({
197
+ width: process.stdout.columns || 120,
198
+ height: process.stdout.rows || 40,
199
+ });
200
+ };
201
+ syncViewport();
202
+ process.stdout.on("resize", syncViewport);
203
+
204
+ // Persist theme changes to config file
205
+ let lastPersistedThemeId = store.getState().ui.themeId;
206
+ store.subscribe(() => {
207
+ const currentThemeId = store.getState().ui.themeId;
208
+ if (currentThemeId && currentThemeId !== lastPersistedThemeId) {
209
+ lastPersistedThemeId = currentThemeId;
210
+ writeConfig({ themeId: currentThemeId });
211
+ }
212
+ });
213
+
147
214
  try {
148
215
  await exitPromise;
149
216
  } finally {