@bytespell/shella 0.1.11 → 0.1.14

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 (119) hide show
  1. package/dist/bin/cli.js +1209 -304
  2. package/dist/bin/cli.js.map +1 -1
  3. package/dist/index.js +1209 -304
  4. package/dist/index.js.map +1 -1
  5. package/dist/public/assets/{_baseUniq-xMBLm_vj.js → _baseUniq-DFrDAdNH.js} +1 -1
  6. package/{public/assets/arc-CCivY36l.js → dist/public/assets/arc-Dn-EBh2M.js} +1 -1
  7. package/{public/assets/architectureDiagram-VXUJARFQ-Cx1ftnZs.js → dist/public/assets/architectureDiagram-VXUJARFQ-cLPhT_VH.js} +1 -1
  8. package/{public/assets/blockDiagram-VD42YOAC-Dd5J4pAa.js → dist/public/assets/blockDiagram-VD42YOAC-D9YWCJd4.js} +1 -1
  9. package/dist/public/assets/{c4Diagram-YG6GDRKO-C4yoJFk1.js → c4Diagram-YG6GDRKO-7iBiFNxv.js} +1 -1
  10. package/dist/public/assets/channel-Bc28uGr5.js +1 -0
  11. package/dist/public/assets/{chunk-4BX2VUAB-CiImuKHD.js → chunk-4BX2VUAB-DHospLZi.js} +1 -1
  12. package/dist/public/assets/{chunk-55IACEB6-V6l1i-_P.js → chunk-55IACEB6-DYi5BOlX.js} +1 -1
  13. package/{public/assets/chunk-B4BG7PRW-DU8Wql9S.js → dist/public/assets/chunk-B4BG7PRW-B9aL4JLb.js} +1 -1
  14. package/{public/assets/chunk-DI55MBZ5-B_Jw-lhj.js → dist/public/assets/chunk-DI55MBZ5-ljO7G9dq.js} +1 -1
  15. package/dist/public/assets/{chunk-FMBD7UC4-C1LlA37K.js → chunk-FMBD7UC4-Cwe-EPLG.js} +1 -1
  16. package/dist/public/assets/{chunk-QN33PNHL-CcgyTGxJ.js → chunk-QN33PNHL-DEnaVKU1.js} +1 -1
  17. package/dist/public/assets/{chunk-QZHKN3VN-C2H0lOPM.js → chunk-QZHKN3VN-CWDdzKRs.js} +1 -1
  18. package/dist/public/assets/{chunk-TZMSLE5B-Cp4Dpzkh.js → chunk-TZMSLE5B-Bpz-P3mQ.js} +1 -1
  19. package/dist/public/assets/classDiagram-2ON5EDUG-DJRc91IB.js +1 -0
  20. package/dist/public/assets/classDiagram-v2-WZHVMYZB-DJRc91IB.js +1 -0
  21. package/dist/public/assets/clone-CkWk6F_f.js +1 -0
  22. package/dist/public/assets/{code-block-IT6T5CEO-DOOuyfxG.js → code-block-IT6T5CEO-Cd4wcD52.js} +1 -1
  23. package/dist/public/assets/{cose-bilkent-S5V4N54A-DefjjRca.js → cose-bilkent-S5V4N54A-BWl9wqzp.js} +1 -1
  24. package/dist/public/assets/{dagre-6UL2VRFP-hHpxpOKX.js → dagre-6UL2VRFP-DGDxxVd8.js} +1 -1
  25. package/dist/public/assets/{diagram-PSM6KHXK-ZgZ2X0Q-.js → diagram-PSM6KHXK-iQjjIACd.js} +1 -1
  26. package/dist/public/assets/{diagram-QEK2KX5R-gCeAqKj1.js → diagram-QEK2KX5R-D-rDIvx4.js} +1 -1
  27. package/dist/public/assets/{diagram-S2PKOQOG-9IyVqMFa.js → diagram-S2PKOQOG-BV8jEZTk.js} +1 -1
  28. package/{public/assets/erDiagram-Q2GNP2WA-Dzm33cik.js → dist/public/assets/erDiagram-Q2GNP2WA-XJ0647Zi.js} +1 -1
  29. package/{public/assets/flowDiagram-NV44I4VS-BamNhi0g.js → dist/public/assets/flowDiagram-NV44I4VS-DSH4uel9.js} +1 -1
  30. package/dist/public/assets/{ganttDiagram-JELNMOA3-CrECl9wX.js → ganttDiagram-JELNMOA3-Cz4yT-vm.js} +1 -1
  31. package/dist/public/assets/{gitGraphDiagram-NY62KEGX-C7_E64uK.js → gitGraphDiagram-NY62KEGX-Y2DLg6xz.js} +1 -1
  32. package/dist/public/assets/{graph-CtTAhETf.js → graph-z0wljCJ_.js} +1 -1
  33. package/dist/public/assets/index-CCsulXgB.css +1 -0
  34. package/dist/public/assets/index-DplAQDze.js +1781 -0
  35. package/dist/public/assets/index-iDg75qrw.js +23 -0
  36. package/dist/public/assets/{infoDiagram-WHAUD3N6-CLOougXO.js → infoDiagram-WHAUD3N6-G5NGCtYh.js} +1 -1
  37. package/dist/public/assets/{journeyDiagram-XKPGCS4Q-g0dISD-I.js → journeyDiagram-XKPGCS4Q-BQ7qAaof.js} +1 -1
  38. package/{public/assets/kanban-definition-3W4ZIXB7-DFIVrLyR.js → dist/public/assets/kanban-definition-3W4ZIXB7-DnMJJT5X.js} +1 -1
  39. package/dist/public/assets/{layout-DolajoeL.js → layout-CbJei3Fo.js} +1 -1
  40. package/dist/public/assets/{linear-B5Hf7uIN.js → linear-3YD7PkfQ.js} +1 -1
  41. package/dist/public/assets/{mermaid.core-DqP3HtOk.js → mermaid.core-DjfX8Ewh.js} +5 -5
  42. package/dist/public/assets/{min-UFvm8GLY.js → min-Chhh-zX-.js} +1 -1
  43. package/dist/public/assets/{mindmap-definition-VGOIOE7T-CnSTRcIt.js → mindmap-definition-VGOIOE7T-CLM5K3ub.js} +1 -1
  44. package/dist/public/assets/{pieDiagram-ADFJNKIX-CuMH9Po8.js → pieDiagram-ADFJNKIX-BFiT_I_m.js} +1 -1
  45. package/{public/assets/quadrantDiagram-AYHSOK5B-6i0SX3Xo.js → dist/public/assets/quadrantDiagram-AYHSOK5B-Be7QQurL.js} +1 -1
  46. package/dist/public/assets/{requirementDiagram-UZGBJVZJ-ChwO2HU-.js → requirementDiagram-UZGBJVZJ-5NrHk2Xy.js} +1 -1
  47. package/{public/assets/sankeyDiagram-TZEHDZUN-DG-8crEL.js → dist/public/assets/sankeyDiagram-TZEHDZUN-DQ8Siqqh.js} +1 -1
  48. package/dist/public/assets/{sequenceDiagram-WL72ISMW-BQH_OfEp.js → sequenceDiagram-WL72ISMW-Bj_sOA9D.js} +1 -1
  49. package/{public/assets/stateDiagram-FKZM4ZOC-CBOkbVQn.js → dist/public/assets/stateDiagram-FKZM4ZOC-DncnQ-99.js} +1 -1
  50. package/dist/public/assets/stateDiagram-v2-4FDKWEC3-BdQ5RGa6.js +1 -0
  51. package/{public/assets/timeline-definition-IT6M3QCI-CkNd8y6W.js → dist/public/assets/timeline-definition-IT6M3QCI-C9uFv_GY.js} +1 -1
  52. package/dist/public/assets/{treemap-KMMF4GRG-D6z_eudj.js → treemap-KMMF4GRG-CTzKLdTW.js} +1 -1
  53. package/{public/assets/xychartDiagram-PRI3JC2R-B5wk4XRt.js → dist/public/assets/xychartDiagram-PRI3JC2R-BP2VkXwS.js} +1 -1
  54. package/dist/public/index.html +15 -2
  55. package/package.json +6 -2
  56. package/public/assets/{_baseUniq-xMBLm_vj.js → _baseUniq-DFrDAdNH.js} +1 -1
  57. package/{dist/public/assets/arc-CCivY36l.js → public/assets/arc-Dn-EBh2M.js} +1 -1
  58. package/{dist/public/assets/architectureDiagram-VXUJARFQ-Cx1ftnZs.js → public/assets/architectureDiagram-VXUJARFQ-cLPhT_VH.js} +1 -1
  59. package/{dist/public/assets/blockDiagram-VD42YOAC-Dd5J4pAa.js → public/assets/blockDiagram-VD42YOAC-D9YWCJd4.js} +1 -1
  60. package/public/assets/{c4Diagram-YG6GDRKO-C4yoJFk1.js → c4Diagram-YG6GDRKO-7iBiFNxv.js} +1 -1
  61. package/public/assets/channel-Bc28uGr5.js +1 -0
  62. package/public/assets/{chunk-4BX2VUAB-CiImuKHD.js → chunk-4BX2VUAB-DHospLZi.js} +1 -1
  63. package/public/assets/{chunk-55IACEB6-V6l1i-_P.js → chunk-55IACEB6-DYi5BOlX.js} +1 -1
  64. package/{dist/public/assets/chunk-B4BG7PRW-DU8Wql9S.js → public/assets/chunk-B4BG7PRW-B9aL4JLb.js} +1 -1
  65. package/{dist/public/assets/chunk-DI55MBZ5-B_Jw-lhj.js → public/assets/chunk-DI55MBZ5-ljO7G9dq.js} +1 -1
  66. package/public/assets/{chunk-FMBD7UC4-C1LlA37K.js → chunk-FMBD7UC4-Cwe-EPLG.js} +1 -1
  67. package/public/assets/{chunk-QN33PNHL-CcgyTGxJ.js → chunk-QN33PNHL-DEnaVKU1.js} +1 -1
  68. package/public/assets/{chunk-QZHKN3VN-C2H0lOPM.js → chunk-QZHKN3VN-CWDdzKRs.js} +1 -1
  69. package/public/assets/{chunk-TZMSLE5B-Cp4Dpzkh.js → chunk-TZMSLE5B-Bpz-P3mQ.js} +1 -1
  70. package/public/assets/classDiagram-2ON5EDUG-DJRc91IB.js +1 -0
  71. package/public/assets/classDiagram-v2-WZHVMYZB-DJRc91IB.js +1 -0
  72. package/public/assets/clone-CkWk6F_f.js +1 -0
  73. package/public/assets/{code-block-IT6T5CEO-DOOuyfxG.js → code-block-IT6T5CEO-Cd4wcD52.js} +1 -1
  74. package/public/assets/{cose-bilkent-S5V4N54A-DefjjRca.js → cose-bilkent-S5V4N54A-BWl9wqzp.js} +1 -1
  75. package/public/assets/{dagre-6UL2VRFP-hHpxpOKX.js → dagre-6UL2VRFP-DGDxxVd8.js} +1 -1
  76. package/public/assets/{diagram-PSM6KHXK-ZgZ2X0Q-.js → diagram-PSM6KHXK-iQjjIACd.js} +1 -1
  77. package/public/assets/{diagram-QEK2KX5R-gCeAqKj1.js → diagram-QEK2KX5R-D-rDIvx4.js} +1 -1
  78. package/public/assets/{diagram-S2PKOQOG-9IyVqMFa.js → diagram-S2PKOQOG-BV8jEZTk.js} +1 -1
  79. package/{dist/public/assets/erDiagram-Q2GNP2WA-Dzm33cik.js → public/assets/erDiagram-Q2GNP2WA-XJ0647Zi.js} +1 -1
  80. package/{dist/public/assets/flowDiagram-NV44I4VS-BamNhi0g.js → public/assets/flowDiagram-NV44I4VS-DSH4uel9.js} +1 -1
  81. package/public/assets/{ganttDiagram-JELNMOA3-CrECl9wX.js → ganttDiagram-JELNMOA3-Cz4yT-vm.js} +1 -1
  82. package/public/assets/{gitGraphDiagram-NY62KEGX-C7_E64uK.js → gitGraphDiagram-NY62KEGX-Y2DLg6xz.js} +1 -1
  83. package/public/assets/{graph-CtTAhETf.js → graph-z0wljCJ_.js} +1 -1
  84. package/public/assets/index-CCsulXgB.css +1 -0
  85. package/public/assets/index-DplAQDze.js +1781 -0
  86. package/public/assets/index-iDg75qrw.js +23 -0
  87. package/public/assets/{infoDiagram-WHAUD3N6-CLOougXO.js → infoDiagram-WHAUD3N6-G5NGCtYh.js} +1 -1
  88. package/public/assets/{journeyDiagram-XKPGCS4Q-g0dISD-I.js → journeyDiagram-XKPGCS4Q-BQ7qAaof.js} +1 -1
  89. package/{dist/public/assets/kanban-definition-3W4ZIXB7-DFIVrLyR.js → public/assets/kanban-definition-3W4ZIXB7-DnMJJT5X.js} +1 -1
  90. package/public/assets/{layout-DolajoeL.js → layout-CbJei3Fo.js} +1 -1
  91. package/public/assets/{linear-B5Hf7uIN.js → linear-3YD7PkfQ.js} +1 -1
  92. package/public/assets/{mermaid.core-DqP3HtOk.js → mermaid.core-DjfX8Ewh.js} +5 -5
  93. package/public/assets/{min-UFvm8GLY.js → min-Chhh-zX-.js} +1 -1
  94. package/public/assets/{mindmap-definition-VGOIOE7T-CnSTRcIt.js → mindmap-definition-VGOIOE7T-CLM5K3ub.js} +1 -1
  95. package/public/assets/{pieDiagram-ADFJNKIX-CuMH9Po8.js → pieDiagram-ADFJNKIX-BFiT_I_m.js} +1 -1
  96. package/{dist/public/assets/quadrantDiagram-AYHSOK5B-6i0SX3Xo.js → public/assets/quadrantDiagram-AYHSOK5B-Be7QQurL.js} +1 -1
  97. package/public/assets/{requirementDiagram-UZGBJVZJ-ChwO2HU-.js → requirementDiagram-UZGBJVZJ-5NrHk2Xy.js} +1 -1
  98. package/{dist/public/assets/sankeyDiagram-TZEHDZUN-DG-8crEL.js → public/assets/sankeyDiagram-TZEHDZUN-DQ8Siqqh.js} +1 -1
  99. package/public/assets/{sequenceDiagram-WL72ISMW-BQH_OfEp.js → sequenceDiagram-WL72ISMW-Bj_sOA9D.js} +1 -1
  100. package/{dist/public/assets/stateDiagram-FKZM4ZOC-CBOkbVQn.js → public/assets/stateDiagram-FKZM4ZOC-DncnQ-99.js} +1 -1
  101. package/public/assets/stateDiagram-v2-4FDKWEC3-BdQ5RGa6.js +1 -0
  102. package/{dist/public/assets/timeline-definition-IT6M3QCI-CkNd8y6W.js → public/assets/timeline-definition-IT6M3QCI-C9uFv_GY.js} +1 -1
  103. package/public/assets/{treemap-KMMF4GRG-D6z_eudj.js → treemap-KMMF4GRG-CTzKLdTW.js} +1 -1
  104. package/{dist/public/assets/xychartDiagram-PRI3JC2R-B5wk4XRt.js → public/assets/xychartDiagram-PRI3JC2R-BP2VkXwS.js} +1 -1
  105. package/public/index.html +15 -2
  106. package/dist/public/assets/channel-CgkYY8Ci.js +0 -1
  107. package/dist/public/assets/classDiagram-2ON5EDUG-BuYVNwmc.js +0 -1
  108. package/dist/public/assets/classDiagram-v2-WZHVMYZB-BuYVNwmc.js +0 -1
  109. package/dist/public/assets/clone-mkrd6lC9.js +0 -1
  110. package/dist/public/assets/index-Czr2q0NP.css +0 -1
  111. package/dist/public/assets/index-kEIJA0pC.js +0 -1781
  112. package/dist/public/assets/stateDiagram-v2-4FDKWEC3-BqHz04Bh.js +0 -1
  113. package/public/assets/channel-CgkYY8Ci.js +0 -1
  114. package/public/assets/classDiagram-2ON5EDUG-BuYVNwmc.js +0 -1
  115. package/public/assets/classDiagram-v2-WZHVMYZB-BuYVNwmc.js +0 -1
  116. package/public/assets/clone-mkrd6lC9.js +0 -1
  117. package/public/assets/index-Czr2q0NP.css +0 -1
  118. package/public/assets/index-kEIJA0pC.js +0 -1781
  119. package/public/assets/stateDiagram-v2-4FDKWEC3-BqHz04Bh.js +0 -1
package/dist/bin/cli.js CHANGED
@@ -15,13 +15,12 @@ import * as p from "@clack/prompts";
15
15
  import express from "express";
16
16
  import { createServer } from "http";
17
17
  import { WebSocketServer } from "ws";
18
- import path2 from "path";
19
- import { fileURLToPath } from "url";
18
+ import path10 from "path";
19
+ import { fileURLToPath as fileURLToPath2 } from "url";
20
20
  import { createExpressMiddleware } from "@trpc/server/adapters/express";
21
21
  import { applyWSSHandler } from "@trpc/server/adapters/ws";
22
- import { agentManager as agentManager2 } from "@bytespell/amux/agents/manager";
22
+ import { muxManager as muxManager3 } from "@bytespell/amux/streams/manager";
23
23
  import { setVerbose, debug } from "@bytespell/amux/lib/logger";
24
- import "@bytespell/amux/db";
25
24
 
26
25
  // src/db/index.ts
27
26
  import Database from "better-sqlite3";
@@ -63,11 +62,37 @@ function ensureDir(dir) {
63
62
  var schema_exports = {};
64
63
  __export(schema_exports, {
65
64
  panes: () => panes,
65
+ streamConfigs: () => streamConfigs,
66
+ streams: () => streams,
66
67
  uiState: () => uiState,
67
68
  windows: () => windows
68
69
  });
69
70
  import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
70
71
  import { sql } from "drizzle-orm";
72
+ var streamConfigs = sqliteTable("stream_configs", {
73
+ id: text("id").primaryKey(),
74
+ name: text("name").notNull(),
75
+ /** Driver type for routing (e.g., 'acp', 'pty', or custom plugin types) */
76
+ driverType: text("driver_type").notNull().default("acp"),
77
+ command: text("command").notNull(),
78
+ args: text("args", { mode: "json" }).$type().default([]),
79
+ env: text("env", { mode: "json" }).$type().default({}),
80
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
81
+ });
82
+ var streams = sqliteTable("streams", {
83
+ id: text("id").primaryKey(),
84
+ directory: text("directory").notNull(),
85
+ streamConfigId: text("stream_config_id").notNull().references(() => streamConfigs.id),
86
+ /** Stream type determined by the driver at creation time. Built-in: 'acp', 'pty' */
87
+ streamType: text("stream_type").notNull(),
88
+ /** Generic driver state (e.g., ACP's session ID for resumption) */
89
+ driverState: text("driver_state", { mode: "json" }).$type(),
90
+ /** Title from stream (via session_info_update for ACP) */
91
+ title: text("title"),
92
+ model: text("model"),
93
+ mode: text("mode"),
94
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
95
+ });
71
96
  var windows = sqliteTable("windows", {
72
97
  id: text("id").primaryKey(),
73
98
  title: text("title").notNull(),
@@ -81,8 +106,12 @@ var windows = sqliteTable("windows", {
81
106
  var panes = sqliteTable("panes", {
82
107
  id: text("id").primaryKey(),
83
108
  windowId: text("window_id").notNull().references(() => windows.id, { onDelete: "cascade" }),
84
- /** Content configuration. Null when pane type not yet selected. */
109
+ /** Static content (browser URL, etc.). Null when no content. */
85
110
  content: text("content", { mode: "json" }).$type(),
111
+ /** Live event stream (amux stream). Null when no stream. */
112
+ eventStream: text("event_stream", { mode: "json" }).$type(),
113
+ /** Renderer ID (e.g., 'builtin:terminal', 'project:custom-viewer'). Null = use default. */
114
+ rendererId: text("renderer_id"),
86
115
  createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
87
116
  });
88
117
  var uiState = sqliteTable("ui_state", {
@@ -97,6 +126,30 @@ sqlite.pragma("journal_mode = WAL");
97
126
  sqlite.pragma("foreign_keys = ON");
98
127
  var db = drizzle(sqlite, { schema: schema_exports });
99
128
  sqlite.exec(`
129
+ -- Stream tables (from amux)
130
+ CREATE TABLE IF NOT EXISTS stream_configs (
131
+ id TEXT PRIMARY KEY,
132
+ name TEXT NOT NULL,
133
+ driver_type TEXT NOT NULL DEFAULT 'acp',
134
+ command TEXT NOT NULL,
135
+ args TEXT DEFAULT '[]',
136
+ env TEXT DEFAULT '{}',
137
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
138
+ );
139
+
140
+ CREATE TABLE IF NOT EXISTS streams (
141
+ id TEXT PRIMARY KEY,
142
+ directory TEXT NOT NULL,
143
+ stream_config_id TEXT NOT NULL REFERENCES stream_configs(id),
144
+ stream_type TEXT NOT NULL,
145
+ driver_state TEXT,
146
+ title TEXT,
147
+ model TEXT,
148
+ mode TEXT,
149
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
150
+ );
151
+
152
+ -- UI tables
100
153
  CREATE TABLE IF NOT EXISTS windows (
101
154
  id TEXT PRIMARY KEY,
102
155
  title TEXT NOT NULL,
@@ -111,6 +164,8 @@ sqlite.exec(`
111
164
  id TEXT PRIMARY KEY,
112
165
  window_id TEXT NOT NULL REFERENCES windows(id) ON DELETE CASCADE,
113
166
  content TEXT,
167
+ event_stream TEXT,
168
+ renderer_id TEXT,
114
169
  created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
115
170
  );
116
171
 
@@ -120,157 +175,686 @@ sqlite.exec(`
120
175
  );
121
176
  `);
122
177
  try {
123
- const tableInfo = sqlite.prepare("PRAGMA table_info(panes)").all();
124
- const hasSessionId = tableInfo.some((col) => col.name === "session_id");
125
- const hasContent = tableInfo.some((col) => col.name === "content");
126
- if (hasSessionId && !hasContent) {
127
- sqlite.exec(`
128
- -- Disable foreign keys temporarily
129
- PRAGMA foreign_keys = OFF;
178
+ const configInfo = sqlite.prepare(`PRAGMA table_info(stream_configs)`).all();
179
+ const hasDriverType = configInfo.some((col) => col.name === "driver_type");
180
+ if (!hasDriverType) {
181
+ console.log("[db] Adding driver_type column to stream_configs...");
182
+ sqlite.exec(`ALTER TABLE stream_configs ADD COLUMN driver_type TEXT NOT NULL DEFAULT 'acp'`);
183
+ sqlite.exec(`UPDATE stream_configs SET driver_type = 'pty' WHERE command = '__shell__'`);
184
+ sqlite.exec(`UPDATE stream_configs SET driver_type = 'mock' WHERE command = '__mock__'`);
185
+ console.log("[db] driver_type migration complete");
186
+ }
187
+ } catch (e) {
188
+ }
189
+ try {
190
+ const tableInfo = sqlite.prepare(`PRAGMA table_info(panes)`).all();
191
+ const hasProcessColumn = tableInfo.some((col) => col.name === "process");
192
+ const hasEventStreamColumn = tableInfo.some((col) => col.name === "event_stream");
193
+ if (hasProcessColumn && !hasEventStreamColumn) {
194
+ console.log("[db] Migrating panes.process to panes.event_stream...");
195
+ sqlite.exec(`ALTER TABLE panes ADD COLUMN event_stream TEXT`);
196
+ const panesWithProcess = sqlite.prepare(`SELECT id, process FROM panes WHERE process IS NOT NULL`).all();
197
+ for (const pane of panesWithProcess) {
198
+ try {
199
+ const process2 = JSON.parse(pane.process);
200
+ if (process2.type === "session" && process2.sessionId) {
201
+ const eventStream = JSON.stringify({ type: "acp", streamId: process2.sessionId });
202
+ sqlite.prepare(`UPDATE panes SET event_stream = ? WHERE id = ?`).run(eventStream, pane.id);
203
+ }
204
+ } catch (e) {
205
+ }
206
+ }
207
+ console.log("[db] Migration complete, migrated", panesWithProcess.length, "panes");
208
+ }
209
+ } catch (e) {
210
+ }
130
211
 
131
- -- Create new table with content column
132
- CREATE TABLE panes_new (
133
- id TEXT PRIMARY KEY,
134
- window_id TEXT NOT NULL REFERENCES windows(id) ON DELETE CASCADE,
135
- content TEXT,
136
- created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
137
- );
212
+ // src/lib/configDiscovery.ts
213
+ import { execSync } from "child_process";
214
+ import { randomUUID } from "crypto";
215
+ function commandExists(cmd) {
216
+ try {
217
+ execSync(`which ${cmd}`, { stdio: "ignore" });
218
+ return true;
219
+ } catch {
220
+ return false;
221
+ }
222
+ }
223
+ var claudeDiscoverer = {
224
+ name: "claude",
225
+ discover() {
226
+ if (!commandExists("claude")) {
227
+ return null;
228
+ }
229
+ return {
230
+ id: randomUUID(),
231
+ name: "claude",
232
+ driverType: "acp",
233
+ command: "npx",
234
+ args: ["@zed-industries/claude-code-acp"],
235
+ env: {},
236
+ createdAt: /* @__PURE__ */ new Date()
237
+ };
238
+ }
239
+ };
240
+ var codexDiscoverer = {
241
+ name: "codex",
242
+ discover() {
243
+ if (!commandExists("codex")) {
244
+ return null;
245
+ }
246
+ return {
247
+ id: randomUUID(),
248
+ name: "codex",
249
+ driverType: "acp",
250
+ command: "npx",
251
+ args: ["@zed-industries/codex-acp"],
252
+ env: {},
253
+ createdAt: /* @__PURE__ */ new Date()
254
+ };
255
+ }
256
+ };
257
+ var geminiDiscoverer = {
258
+ name: "gemini",
259
+ discover() {
260
+ if (!commandExists("gemini")) {
261
+ return null;
262
+ }
263
+ return {
264
+ id: randomUUID(),
265
+ name: "gemini",
266
+ driverType: "acp",
267
+ command: "gemini",
268
+ args: ["--experimental-acp"],
269
+ env: {},
270
+ createdAt: /* @__PURE__ */ new Date()
271
+ };
272
+ }
273
+ };
274
+ var openCodeDiscoverer = {
275
+ name: "opencode",
276
+ discover() {
277
+ if (!commandExists("opencode")) {
278
+ return null;
279
+ }
280
+ return {
281
+ id: randomUUID(),
282
+ name: "opencode",
283
+ driverType: "acp",
284
+ command: "opencode",
285
+ args: ["acp"],
286
+ env: {},
287
+ createdAt: /* @__PURE__ */ new Date()
288
+ };
289
+ }
290
+ };
291
+ var shellDiscoverer = {
292
+ name: "Shell",
293
+ discover() {
294
+ return {
295
+ id: "shell",
296
+ name: "shell",
297
+ driverType: "pty",
298
+ command: "",
299
+ // PTY driver ignores command, spawns user's $SHELL
300
+ args: [],
301
+ env: {},
302
+ createdAt: /* @__PURE__ */ new Date()
303
+ };
304
+ }
305
+ };
306
+ var builtInDiscoverers = [
307
+ claudeDiscoverer,
308
+ codexDiscoverer,
309
+ geminiDiscoverer,
310
+ openCodeDiscoverer,
311
+ shellDiscoverer
312
+ ];
313
+ function discoverAndRegisterConfigs(discoverers) {
314
+ const existing = db.select().from(streamConfigs).all();
315
+ let added = 0;
316
+ for (const discoverer of discoverers) {
317
+ const config = discoverer.discover();
318
+ if (!config) {
319
+ continue;
320
+ }
321
+ const alreadyExists = existing.some(
322
+ (e) => e.command === config.command && JSON.stringify(e.args) === JSON.stringify(config.args)
323
+ );
324
+ if (!alreadyExists) {
325
+ db.insert(streamConfigs).values({
326
+ id: config.id,
327
+ name: config.name,
328
+ driverType: config.driverType,
329
+ command: config.command,
330
+ args: config.args,
331
+ env: config.env,
332
+ createdAt: config.createdAt
333
+ }).run();
334
+ console.log(`[config] Discovered: ${config.name}`);
335
+ added++;
336
+ }
337
+ }
338
+ if (added > 0) {
339
+ console.log(`[config] ${added} new stream config(s) registered`);
340
+ }
341
+ }
138
342
 
139
- -- Copy data, converting session_id to content JSON
140
- INSERT INTO panes_new (id, window_id, content, created_at)
141
- SELECT
142
- id,
143
- window_id,
144
- CASE
145
- WHEN session_id IS NOT NULL THEN json_object('type', 'session', 'sessionId', session_id)
146
- ELSE NULL
147
- END,
148
- created_at
149
- FROM panes;
343
+ // src/lib/driverWatcher.ts
344
+ import chokidar from "chokidar";
345
+ import * as path3 from "path";
346
+ import * as os2 from "os";
347
+ import * as fs3 from "fs/promises";
348
+ import { EventEmitter } from "events";
150
349
 
151
- -- Drop old table and rename new
152
- DROP TABLE panes;
153
- ALTER TABLE panes_new RENAME TO panes;
350
+ // src/lib/driverCompiler.ts
351
+ import { build } from "esbuild";
352
+ import * as fs2 from "fs/promises";
353
+ import * as path2 from "path";
354
+ import { fileURLToPath } from "url";
355
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
356
+ async function compileDriver(pluginDir) {
357
+ const serverPath = path2.join(pluginDir, "server.ts");
358
+ try {
359
+ await fs2.access(serverPath);
360
+ } catch {
361
+ return null;
362
+ }
363
+ const metaPath = path2.join(pluginDir, "meta.json");
364
+ let meta;
365
+ try {
366
+ const metaContent = await fs2.readFile(metaPath, "utf-8");
367
+ meta = JSON.parse(metaContent);
368
+ } catch (err) {
369
+ throw new Error(
370
+ `Failed to read meta.json for ${pluginDir}: ${err instanceof Error ? err.message : String(err)}`
371
+ );
372
+ }
373
+ if (!meta.id || typeof meta.id !== "string") {
374
+ throw new Error(`Plugin in ${pluginDir}: meta.json requires "id" field`);
375
+ }
376
+ if (!meta.name || typeof meta.name !== "string") {
377
+ throw new Error(`Plugin in ${pluginDir}: meta.json requires "name" field`);
378
+ }
379
+ const outDir = path2.join(pluginDir, ".shella");
380
+ const outFile = path2.join(outDir, "server.mjs");
381
+ await fs2.mkdir(outDir, { recursive: true });
382
+ const pkgPath = path2.join(pluginDir, "package.json");
383
+ let hasPackageJson = false;
384
+ try {
385
+ await fs2.access(pkgPath);
386
+ hasPackageJson = true;
387
+ } catch {
388
+ }
389
+ const shellaRoot = path2.resolve(__dirname, "../..");
390
+ const sdkPath = path2.resolve(shellaRoot, "../sdk/dist");
391
+ await build({
392
+ entryPoints: [serverPath],
393
+ bundle: true,
394
+ write: true,
395
+ format: "esm",
396
+ platform: "node",
397
+ target: "node20",
398
+ outfile: outFile,
399
+ alias: {
400
+ "@bytespell/shella-sdk": path2.join(sdkPath, "index.js"),
401
+ "@bytespell/shella-sdk/server": path2.join(sdkPath, "server.js")
402
+ },
403
+ // If plugin has package.json, look for node_modules
404
+ nodePaths: hasPackageJson ? [path2.join(pluginDir, "node_modules")] : [],
405
+ minify: false
406
+ });
407
+ return {
408
+ id: meta.id,
409
+ name: meta.name,
410
+ filePath: outFile,
411
+ pluginDir
412
+ };
413
+ }
154
414
 
155
- -- Re-enable foreign keys
156
- PRAGMA foreign_keys = ON;
157
- `);
158
- console.log("[shella-db] Migrated panes from session_id to content column");
415
+ // src/lib/driverWatcher.ts
416
+ import { muxManager } from "@bytespell/amux/streams/manager";
417
+ import { eq } from "drizzle-orm";
418
+ var DriverEventEmitter = class extends EventEmitter {
419
+ emit(event, data) {
420
+ return super.emit(event, data);
421
+ }
422
+ on(event, listener) {
423
+ return super.on(event, listener);
424
+ }
425
+ off(event, listener) {
426
+ return super.off(event, listener);
427
+ }
428
+ };
429
+ var loadedDrivers = /* @__PURE__ */ new Map();
430
+ async function loadDriver(compiled) {
431
+ const entry = loadedDrivers.get(compiled.pluginDir);
432
+ const version = (entry?.version ?? 0) + 1;
433
+ const url = `file://${compiled.filePath}?v=${version}`;
434
+ const module = await import(
435
+ /* @vite-ignore */
436
+ url
437
+ );
438
+ const driver = module.default;
439
+ if (!driver || typeof driver.start !== "function") {
440
+ throw new Error(`Plugin driver must export default with start() method`);
441
+ }
442
+ driver.type = compiled.id;
443
+ loadedDrivers.set(compiled.pluginDir, { id: compiled.id, version });
444
+ return driver;
445
+ }
446
+ function ensureStreamConfig(compiled) {
447
+ const existing = db.select().from(streamConfigs).where(eq(streamConfigs.id, compiled.id)).get();
448
+ if (!existing) {
449
+ db.insert(streamConfigs).values({
450
+ id: compiled.id,
451
+ name: compiled.name,
452
+ driverType: compiled.id,
453
+ // Driver type matches plugin id
454
+ command: "",
455
+ // Plugin drivers don't spawn processes
456
+ args: [],
457
+ env: {},
458
+ createdAt: /* @__PURE__ */ new Date()
459
+ }).run();
460
+ } else if (existing.name !== compiled.name) {
461
+ db.update(streamConfigs).set({ name: compiled.name }).where(eq(streamConfigs.id, compiled.id)).run();
462
+ }
463
+ }
464
+ function removeStreamConfig(id) {
465
+ db.delete(streamConfigs).where(eq(streamConfigs.id, id)).run();
466
+ }
467
+ function watchDrivers(emitter) {
468
+ const userPath = path3.join(os2.homedir(), ".shella", "renderers");
469
+ const pendingCompiles = /* @__PURE__ */ new Map();
470
+ const watcher = chokidar.watch(userPath, {
471
+ ignoreInitial: false,
472
+ persistent: true,
473
+ awaitWriteFinish: {
474
+ stabilityThreshold: 150,
475
+ pollInterval: 50
476
+ },
477
+ depth: 3
478
+ // Watch inside plugin folders
479
+ });
480
+ const getPluginDir = (filePath) => {
481
+ const relative3 = path3.relative(userPath, filePath);
482
+ const parts = relative3.split(path3.sep);
483
+ if (parts.length >= 1 && parts[0]) {
484
+ return path3.join(userPath, parts[0]);
485
+ }
486
+ return null;
487
+ };
488
+ const isServerFile = (filePath) => {
489
+ const relative3 = path3.relative(userPath, filePath);
490
+ const parts = relative3.split(path3.sep);
491
+ return parts.length >= 2 && (parts[1] === "server.ts" || parts[1]?.startsWith("server/") || filePath.includes("/server/"));
492
+ };
493
+ const scheduleCompile = (pluginDir) => {
494
+ if (pendingCompiles.has(pluginDir)) {
495
+ clearTimeout(pendingCompiles.get(pluginDir));
496
+ }
497
+ pendingCompiles.set(
498
+ pluginDir,
499
+ setTimeout(async () => {
500
+ pendingCompiles.delete(pluginDir);
501
+ await handleDriverChange(pluginDir);
502
+ }, 150)
503
+ );
504
+ };
505
+ const handleDriverChange = async (pluginDir) => {
506
+ try {
507
+ const compiled = await compileDriver(pluginDir);
508
+ if (!compiled) return;
509
+ const existing = loadedDrivers.get(pluginDir);
510
+ if (existing) {
511
+ muxManager.unregisterDriver(existing.id);
512
+ }
513
+ const driver = await loadDriver(compiled);
514
+ muxManager.registerDriver(driver);
515
+ ensureStreamConfig(compiled);
516
+ if (!existing) {
517
+ console.log(`[drivers] Added: ${compiled.id} (${compiled.name})`);
518
+ emitter.emit("driver:added", { id: compiled.id, name: compiled.name });
519
+ } else {
520
+ console.log(`[drivers] Updated: ${compiled.id} (${compiled.name})`);
521
+ emitter.emit("driver:updated", { id: compiled.id, name: compiled.name });
522
+ }
523
+ } catch (err) {
524
+ console.error(`[drivers] Error compiling ${pluginDir}:`, err);
525
+ emitter.emit("driver:error", {
526
+ pluginDir,
527
+ error: err instanceof Error ? err : new Error(String(err))
528
+ });
529
+ }
530
+ };
531
+ watcher.on("add", (filePath) => {
532
+ if (isServerFile(filePath)) {
533
+ const pluginDir = getPluginDir(filePath);
534
+ if (pluginDir) scheduleCompile(pluginDir);
535
+ }
536
+ });
537
+ watcher.on("change", (filePath) => {
538
+ if (isServerFile(filePath)) {
539
+ const pluginDir = getPluginDir(filePath);
540
+ if (pluginDir) scheduleCompile(pluginDir);
541
+ }
542
+ });
543
+ watcher.on("unlink", (filePath) => {
544
+ if (filePath.endsWith("server.ts")) {
545
+ const pluginDir = getPluginDir(filePath);
546
+ if (pluginDir) {
547
+ const entry = loadedDrivers.get(pluginDir);
548
+ if (entry) {
549
+ muxManager.unregisterDriver(entry.id);
550
+ removeStreamConfig(entry.id);
551
+ loadedDrivers.delete(pluginDir);
552
+ console.log(`[drivers] Removed: ${entry.id}`);
553
+ emitter.emit("driver:removed", { id: entry.id });
554
+ }
555
+ }
556
+ }
557
+ });
558
+ watcher.on("error", (err) => {
559
+ console.error("[drivers] Watcher error:", err);
560
+ });
561
+ return () => {
562
+ for (const timeout of pendingCompiles.values()) {
563
+ clearTimeout(timeout);
564
+ }
565
+ pendingCompiles.clear();
566
+ watcher.close();
567
+ };
568
+ }
569
+ async function scanDrivers() {
570
+ const userPath = path3.join(os2.homedir(), ".shella", "renderers");
571
+ try {
572
+ const entries = await fs3.readdir(userPath, { withFileTypes: true });
573
+ for (const entry of entries) {
574
+ if (!entry.isDirectory()) continue;
575
+ const pluginDir = path3.join(userPath, entry.name);
576
+ try {
577
+ const compiled = await compileDriver(pluginDir);
578
+ if (compiled) {
579
+ const driver = await loadDriver(compiled);
580
+ muxManager.registerDriver(driver);
581
+ ensureStreamConfig(compiled);
582
+ console.log(`[drivers] Loaded: ${compiled.id} (${compiled.name})`);
583
+ }
584
+ } catch (err) {
585
+ console.error(`[drivers] Error loading ${pluginDir}:`, err);
586
+ }
587
+ }
588
+ } catch {
159
589
  }
160
- } catch {
161
590
  }
162
591
 
163
592
  // src/amuxBridge.ts
164
- import { randomUUID } from "crypto";
165
- import { eq, inArray } from "drizzle-orm";
166
- import { db as db2, schema } from "@bytespell/amux/db";
167
- import { agentManager } from "@bytespell/amux/agents/manager";
168
- import { clearEventsForSession } from "@bytespell/amux/agents/eventStore";
169
- import { listFilesForAutocomplete } from "@bytespell/amux/trpc/files";
170
- var { sessions, agentConfigs, appState } = schema;
593
+ import { randomUUID as randomUUID2 } from "crypto";
594
+ import path5 from "path";
595
+ import fs5 from "fs";
596
+ import { eq as eq2, inArray } from "drizzle-orm";
597
+
598
+ // src/lib/files.ts
599
+ import * as fs4 from "fs/promises";
600
+ import * as path4 from "path";
601
+ import { glob } from "glob";
602
+ async function listFilesForAutocomplete(workingDir, partialPath, limit) {
603
+ const normalized = partialPath.startsWith("./") ? partialPath.slice(2) : partialPath;
604
+ if (normalized.includes("/")) {
605
+ return listFilesPrefix(workingDir, normalized, limit);
606
+ }
607
+ return listFilesFuzzy(workingDir, normalized, limit);
608
+ }
609
+ async function listFilesPrefix(workingDir, partialPath, limit) {
610
+ const lastSlash = partialPath.lastIndexOf("/");
611
+ const dirPart = lastSlash >= 0 ? partialPath.slice(0, lastSlash) : "";
612
+ const prefix = lastSlash >= 0 ? partialPath.slice(lastSlash + 1) : partialPath;
613
+ const targetDir = path4.resolve(workingDir, dirPart);
614
+ const normalizedWorkingDir = path4.resolve(workingDir);
615
+ const normalizedTargetDir = path4.resolve(targetDir);
616
+ if (!normalizedTargetDir.startsWith(normalizedWorkingDir)) {
617
+ return [];
618
+ }
619
+ try {
620
+ const entries = await fs4.readdir(targetDir, { withFileTypes: true });
621
+ const matching = entries.filter((e) => !e.name.startsWith(".")).filter((e) => prefix === "" || e.name.toLowerCase().startsWith(prefix.toLowerCase())).slice(0, limit * 2).map((e) => ({
622
+ name: e.name,
623
+ path: dirPart ? `${dirPart}/${e.name}` : e.name,
624
+ isDirectory: e.isDirectory()
625
+ }));
626
+ matching.sort((a, b) => {
627
+ if (a.isDirectory !== b.isDirectory) {
628
+ return a.isDirectory ? -1 : 1;
629
+ }
630
+ return a.name.localeCompare(b.name);
631
+ });
632
+ return matching.slice(0, limit);
633
+ } catch {
634
+ return [];
635
+ }
636
+ }
637
+ async function listFilesFuzzy(workingDir, query, limit) {
638
+ if (!query.trim()) return [];
639
+ try {
640
+ const pattern = `{*${query}*,**/*${query}*}`;
641
+ const matches = await glob(pattern, {
642
+ cwd: workingDir,
643
+ nodir: false,
644
+ dot: false,
645
+ // Skip hidden files
646
+ ignore: ["**/node_modules/**", "**/.git/**"],
647
+ maxDepth: 10
648
+ });
649
+ const results = [];
650
+ for (const match of matches.slice(0, limit * 2)) {
651
+ try {
652
+ const fullPath = path4.join(workingDir, match);
653
+ const stat2 = await fs4.stat(fullPath);
654
+ results.push({
655
+ name: path4.basename(match),
656
+ path: match,
657
+ isDirectory: stat2.isDirectory()
658
+ });
659
+ } catch {
660
+ }
661
+ }
662
+ results.sort((a, b) => {
663
+ if (a.isDirectory !== b.isDirectory) {
664
+ return a.isDirectory ? -1 : 1;
665
+ }
666
+ const aDepth = a.path.split("/").length;
667
+ const bDepth = b.path.split("/").length;
668
+ if (aDepth !== bDepth) {
669
+ return aDepth - bDepth;
670
+ }
671
+ return a.path.localeCompare(b.path);
672
+ });
673
+ return results.slice(0, limit);
674
+ } catch {
675
+ return [];
676
+ }
677
+ }
678
+
679
+ // src/amuxBridge.ts
680
+ import { muxManager as muxManager2 } from "@bytespell/amux/streams/manager";
681
+ import { isAcpEvent } from "@bytespell/amux/types";
682
+ function getStreamStorageDir(streamId) {
683
+ return path5.join(getDataDir(), "streams", streamId);
684
+ }
685
+ function generateTitleFromMessage(message) {
686
+ const cleaned = message.replace(/@[\w./~-]+/g, "").replace(/\s+/g, " ").trim();
687
+ const firstLine = cleaned.split("\n")[0] || cleaned;
688
+ if (firstLine.length <= 50) return firstLine;
689
+ const truncated = firstLine.slice(0, 50);
690
+ const lastSpace = truncated.lastIndexOf(" ");
691
+ return lastSpace > 20 ? truncated.slice(0, lastSpace) + "..." : truncated + "...";
692
+ }
171
693
  function createAmuxBridge() {
172
- return {
173
- async getSessions(ids) {
694
+ const bridge = {
695
+ async getStreams(ids) {
174
696
  if (ids.length === 0) return [];
175
- return db2.select().from(sessions).where(inArray(sessions.id, ids)).all();
697
+ return db.select().from(streams).where(inArray(streams.id, ids)).all();
176
698
  },
177
- async getSession(id) {
178
- return db2.select().from(sessions).where(eq(sessions.id, id)).get() ?? null;
699
+ async getStream(id) {
700
+ return db.select().from(streams).where(eq2(streams.id, id)).get() ?? null;
179
701
  },
180
- async createSession(params) {
181
- let configId = params.agentConfigId;
702
+ async createStream(params) {
703
+ let configId = params.streamConfigId;
182
704
  if (!configId) {
183
- const lastUsedRow = db2.select().from(appState).where(eq(appState.key, "last_used_agent_config_id")).get();
705
+ const lastUsedRow = db.select().from(uiState).where(eq2(uiState.key, "last_used_stream_config_id")).get();
184
706
  if (lastUsedRow?.value) {
185
- const config2 = db2.select().from(agentConfigs).where(eq(agentConfigs.id, lastUsedRow.value)).get();
707
+ const config2 = db.select().from(streamConfigs).where(eq2(streamConfigs.id, lastUsedRow.value)).get();
186
708
  if (config2) configId = config2.id;
187
709
  }
188
710
  if (!configId) {
189
- const firstConfig = db2.select().from(agentConfigs).get();
711
+ const firstConfig = db.select().from(streamConfigs).get();
190
712
  if (!firstConfig) {
191
- throw new Error("No agent configs available");
713
+ throw new Error("No stream configs available");
192
714
  }
193
715
  configId = firstConfig.id;
194
716
  }
195
717
  } else {
196
- db2.insert(appState).values({ key: "last_used_agent_config_id", value: configId }).onConflictDoUpdate({ target: appState.key, set: { value: configId } }).run();
718
+ db.insert(uiState).values({ key: "last_used_stream_config_id", value: configId }).onConflictDoUpdate({ target: uiState.key, set: { value: configId } }).run();
197
719
  }
198
- const id = params.id ?? randomUUID();
720
+ const id = params.id ?? randomUUID2();
199
721
  const now = /* @__PURE__ */ new Date();
200
- const config = db2.select().from(agentConfigs).where(eq(agentConfigs.id, configId)).get();
201
- const initialTitle = config?.streamType === "pty" ? params.directory.split("/").pop() || null : null;
202
- db2.insert(sessions).values({
722
+ const config = db.select().from(streamConfigs).where(eq2(streamConfigs.id, configId)).get();
723
+ const streamType = config?.driverType === "pty" ? "pty" : "acp";
724
+ const initialTitle = streamType === "pty" ? params.directory.split("/").pop() || null : null;
725
+ db.insert(streams).values({
203
726
  id,
204
727
  directory: params.directory,
205
- agentConfigId: configId,
206
- acpSessionId: null,
728
+ streamConfigId: configId,
729
+ streamType,
730
+ driverState: null,
207
731
  title: initialTitle,
208
732
  model: null,
209
733
  mode: null,
210
734
  createdAt: now
211
735
  }).run();
212
- return db2.select().from(sessions).where(eq(sessions.id, id)).get();
736
+ return db.select().from(streams).where(eq2(streams.id, id)).get();
213
737
  },
214
- async deleteSession(id) {
215
- await agentManager.stopForSession(id);
216
- clearEventsForSession(id);
217
- db2.delete(sessions).where(eq(sessions.id, id)).run();
738
+ async deleteStream(id) {
739
+ await muxManager2.stopForStream(id);
740
+ const storageDir = getStreamStorageDir(id);
741
+ try {
742
+ if (fs5.existsSync(storageDir)) {
743
+ fs5.rmSync(storageDir, { recursive: true });
744
+ }
745
+ } catch {
746
+ }
747
+ db.delete(streams).where(eq2(streams.id, id)).run();
218
748
  },
219
- async getAgentConfigs() {
220
- return db2.select().from(agentConfigs).all();
749
+ async getStreamConfigs() {
750
+ return db.select().from(streamConfigs).all();
221
751
  },
222
- async startAgent(sessionId) {
223
- return agentManager.startForSession(sessionId);
752
+ async startStream(streamId) {
753
+ const dbStream = db.select().from(streams).where(eq2(streams.id, streamId)).get();
754
+ if (!dbStream) throw new Error(`Stream ${streamId} not found`);
755
+ const dbConfig = db.select().from(streamConfigs).where(eq2(streamConfigs.id, dbStream.streamConfigId)).get();
756
+ if (!dbConfig) throw new Error(`Stream config ${dbStream.streamConfigId} not found`);
757
+ const storageDir = getStreamStorageDir(streamId);
758
+ const emit = (event) => {
759
+ if (isAcpEvent(event) && event.sessionUpdate === "session_info_update") {
760
+ const title = event.title;
761
+ if (title !== void 0) {
762
+ db.update(streams).set({ title }).where(eq2(streams.id, streamId)).run();
763
+ }
764
+ }
765
+ };
766
+ const handle = await muxManager2.startStream({
767
+ streamId,
768
+ driverType: dbConfig.driverType,
769
+ config: {
770
+ id: dbConfig.id,
771
+ name: dbConfig.name,
772
+ command: dbConfig.command,
773
+ args: dbConfig.args ?? [],
774
+ env: dbConfig.env ?? {}
775
+ },
776
+ cwd: dbStream.directory,
777
+ restoredState: dbStream.driverState,
778
+ emit,
779
+ storageDir
780
+ });
781
+ const driverState = muxManager2.getDriverState(streamId);
782
+ if (driverState !== void 0) {
783
+ db.update(streams).set({ driverState }).where(eq2(streams.id, streamId)).run();
784
+ }
785
+ const replayData = muxManager2.getReplayData(streamId);
786
+ const replayEvents = Array.isArray(replayData) ? replayData : [];
787
+ const scrollback = typeof replayData === "string" ? replayData : muxManager2.getTerminalScrollback(streamId);
788
+ return {
789
+ acpSessionId: handle.stream.acpSessionId,
790
+ replayEvents,
791
+ scrollback,
792
+ models: handle.stream.models,
793
+ modes: handle.stream.modes
794
+ };
224
795
  },
225
- async stopAgent(sessionId) {
226
- await agentManager.stopForSession(sessionId);
796
+ async stopStream(streamId) {
797
+ const driverState = muxManager2.getDriverState(streamId);
798
+ if (driverState !== void 0) {
799
+ db.update(streams).set({ driverState }).where(eq2(streams.id, streamId)).run();
800
+ }
801
+ await muxManager2.stopForStream(streamId);
227
802
  },
228
- async prompt(sessionId, message) {
229
- await agentManager.prompt(sessionId, message);
803
+ async input(streamId, message) {
804
+ const stream = db.select().from(streams).where(eq2(streams.id, streamId)).get();
805
+ if (!stream) throw new Error(`Stream ${streamId} not found`);
806
+ if (!stream.title) {
807
+ const title = generateTitleFromMessage(message);
808
+ if (title) {
809
+ db.update(streams).set({ title }).where(eq2(streams.id, streamId)).run();
810
+ }
811
+ }
812
+ await muxManager2.input(streamId, message);
230
813
  },
231
- async cancel(sessionId) {
232
- await agentManager.cancel(sessionId);
814
+ async cancel(streamId) {
815
+ await muxManager2.cancel(streamId);
233
816
  },
234
- async setMode(sessionId, modeId) {
235
- await agentManager.setMode(sessionId, modeId);
817
+ async setMode(streamId, modeId) {
818
+ await muxManager2.setMode(streamId, modeId);
236
819
  },
237
- async setModel(sessionId, modelId) {
238
- await agentManager.setModel(sessionId, modelId);
820
+ async setModel(streamId, modelId) {
821
+ await muxManager2.setModel(streamId, modelId);
239
822
  },
240
- respondPermission(sessionId, requestId, optionId) {
241
- agentManager.respondPermission(sessionId, requestId, optionId);
823
+ respondPermission(streamId, requestId, optionId) {
824
+ muxManager2.respondPermission(streamId, requestId, optionId);
242
825
  },
243
- subscribeToSession(sessionId, callback) {
826
+ subscribeToStream(streamId, callback) {
244
827
  const handler = (event) => {
245
- if (event.sessionId === sessionId) {
246
- callback(event.update);
828
+ if (event.streamId === streamId) {
829
+ callback(event);
247
830
  }
248
831
  };
249
- agentManager.on("update", handler);
250
- return () => agentManager.off("update", handler);
832
+ muxManager2.on("update", handler);
833
+ return () => muxManager2.off("update", handler);
251
834
  },
252
- async listFilesForAutocomplete(sessionId, partialPath, limit) {
253
- const session = db2.select().from(sessions).where(eq(sessions.id, sessionId)).get();
254
- if (!session) throw new Error(`Session ${sessionId} not found`);
255
- return listFilesForAutocomplete(session.directory, partialPath, limit);
835
+ async listFilesForAutocomplete(streamId, partialPath, limit) {
836
+ const stream = db.select().from(streams).where(eq2(streams.id, streamId)).get();
837
+ if (!stream) throw new Error(`Stream ${streamId} not found`);
838
+ return listFilesForAutocomplete(stream.directory, partialPath, limit);
256
839
  },
257
840
  // Terminal operations
258
- terminalWrite(sessionId, data) {
259
- agentManager.terminalWrite(sessionId, data);
841
+ terminalWrite(streamId, data) {
842
+ muxManager2.terminalWrite(streamId, data);
260
843
  },
261
- terminalResize(sessionId, cols, rows) {
262
- agentManager.terminalResize(sessionId, cols, rows);
844
+ terminalResize(streamId, cols, rows) {
845
+ muxManager2.terminalResize(streamId, cols, rows);
263
846
  },
264
- getTerminalScrollback(sessionId) {
265
- return agentManager.getTerminalScrollback(sessionId);
847
+ getTerminalScrollback(streamId) {
848
+ return muxManager2.getTerminalScrollback(streamId);
266
849
  },
267
- isTerminalSession(sessionId) {
268
- return agentManager.isTerminalSession(sessionId);
850
+ isTerminalStream(streamId) {
851
+ return muxManager2.isTerminalStream(streamId);
269
852
  },
270
- updateSessionTitle(sessionId, title) {
271
- db2.update(sessions).set({ title }).where(eq(sessions.id, sessionId)).run();
853
+ updateStreamTitle(streamId, title) {
854
+ db.update(streams).set({ title }).where(eq2(streams.id, streamId)).run();
272
855
  }
273
856
  };
857
+ return bridge;
274
858
  }
275
859
 
276
860
  // src/trpc/trpc.ts
@@ -281,8 +865,8 @@ var publicProcedure = t.procedure;
281
865
 
282
866
  // src/trpc/windows.ts
283
867
  import { z } from "zod";
284
- import { randomUUID as randomUUID2 } from "crypto";
285
- import { eq as eq2 } from "drizzle-orm";
868
+ import { randomUUID as randomUUID3 } from "crypto";
869
+ import { eq as eq3 } from "drizzle-orm";
286
870
 
287
871
  // client/lib/layout.ts
288
872
  function createLeaf(paneId) {
@@ -297,13 +881,13 @@ function getAllPaneIds(node) {
297
881
  }
298
882
  return [...getAllPaneIds(node.first), ...getAllPaneIds(node.second)];
299
883
  }
300
- function findPanePath(node, paneId, path3 = []) {
884
+ function findPanePath(node, paneId, path11 = []) {
301
885
  if (node.type === "leaf") {
302
- return node.paneId === paneId ? path3 : null;
886
+ return node.paneId === paneId ? path11 : null;
303
887
  }
304
- const firstPath = findPanePath(node.first, paneId, [...path3, "first"]);
888
+ const firstPath = findPanePath(node.first, paneId, [...path11, "first"]);
305
889
  if (firstPath) return firstPath;
306
- return findPanePath(node.second, paneId, [...path3, "second"]);
890
+ return findPanePath(node.second, paneId, [...path11, "second"]);
307
891
  }
308
892
  function replacePaneWithNode(root, paneId, newNode) {
309
893
  if (root.type === "leaf") {
@@ -340,15 +924,15 @@ function removePane(root, paneId) {
340
924
  }
341
925
  return root;
342
926
  }
343
- function updateRatio(root, path3, newRatio) {
344
- if (path3.length === 0) {
927
+ function updateRatio(root, path11, newRatio) {
928
+ if (path11.length === 0) {
345
929
  if (root.type === "branch") {
346
930
  return { ...root, ratio: Math.max(10, Math.min(90, newRatio)) };
347
931
  }
348
932
  return root;
349
933
  }
350
934
  if (root.type === "leaf") return root;
351
- const [next, ...rest] = path3;
935
+ const [next, ...rest] = path11;
352
936
  if (next === "first") {
353
937
  return { ...root, first: updateRatio(root.first, rest, newRatio) };
354
938
  } else {
@@ -356,11 +940,11 @@ function updateRatio(root, path3, newRatio) {
356
940
  }
357
941
  }
358
942
  function getAdjacentPane(root, currentPaneId, direction) {
359
- const path3 = findPanePath(root, currentPaneId);
360
- if (!path3) return null;
943
+ const path11 = findPanePath(root, currentPaneId);
944
+ if (!path11) return null;
361
945
  let node = root;
362
946
  const pathNodes = [];
363
- for (const step of path3) {
947
+ for (const step of path11) {
364
948
  if (node.type === "branch") {
365
949
  pathNodes.push({ node, childSide: step });
366
950
  node = step === "first" ? node.first : node.second;
@@ -406,71 +990,76 @@ function generateTitle(existingCount, directory) {
406
990
  }
407
991
  var windowsRouter = router({
408
992
  /**
409
- * List all windows with their panes and sessions.
993
+ * List all windows with their panes and streams.
410
994
  */
411
995
  list: publicProcedure.query(async () => {
412
996
  const allWindows = db.select().from(windows).orderBy(windows.createdAt).all();
413
997
  const allPanes = db.select().from(panes).all();
414
- const activeRow = db.select().from(uiState).where(eq2(uiState.key, "active_window_id")).get();
415
- const sessionIds = [...new Set(
416
- allPanes.map((p2) => p2.content?.type === "session" ? p2.content.sessionId : null).filter((id) => id !== null)
998
+ const activeRow = db.select().from(uiState).where(eq3(uiState.key, "active_window_id")).get();
999
+ const streamIds = [...new Set(
1000
+ allPanes.map((p2) => p2.eventStream?.streamId ?? null).filter((id) => id !== null)
417
1001
  )];
418
- const sessionsData = await amux.getSessions(sessionIds);
419
- const sessionMap = new Map(sessionsData.map((s) => [s.id, s]));
420
- const agentConfigs2 = await amux.getAgentConfigs();
1002
+ const streamsData = await amux.getStreams(streamIds);
1003
+ const streamMap = new Map(streamsData.map((s) => [s.id, s]));
1004
+ const streamConfigs2 = await amux.getStreamConfigs();
421
1005
  return {
422
1006
  windows: allWindows.map((w) => ({
423
1007
  ...w,
424
1008
  panes: allPanes.filter((p2) => p2.windowId === w.id).map((p2) => ({
425
1009
  ...p2,
426
- // Enrich session-type content with full session data
427
- session: p2.content?.type === "session" ? sessionMap.get(p2.content.sessionId) ?? null : null
1010
+ // Enrich eventStream with full stream data
1011
+ stream: p2.eventStream ? streamMap.get(p2.eventStream.streamId) ?? null : null
428
1012
  }))
429
1013
  })),
430
1014
  activeWindowId: activeRow?.value ?? allWindows[allWindows.length - 1]?.id ?? null,
431
- agentConfigs: agentConfigs2
1015
+ streamConfigs: streamConfigs2,
1016
+ // Legacy alias
1017
+ agentConfigs: streamConfigs2
432
1018
  };
433
1019
  }),
434
1020
  /**
435
1021
  * Get a single window by ID.
436
1022
  */
437
1023
  get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
438
- const window = db.select().from(windows).where(eq2(windows.id, input.id)).get();
1024
+ const window = db.select().from(windows).where(eq3(windows.id, input.id)).get();
439
1025
  if (!window) {
440
1026
  throw new Error(`Window ${input.id} not found`);
441
1027
  }
442
- const windowPanes = db.select().from(panes).where(eq2(panes.windowId, input.id)).all();
443
- const sessionIds = windowPanes.map((p2) => p2.content?.type === "session" ? p2.content.sessionId : null).filter((id) => id !== null);
444
- const sessionsData = await amux.getSessions(sessionIds);
445
- const sessionMap = new Map(sessionsData.map((s) => [s.id, s]));
1028
+ const windowPanes = db.select().from(panes).where(eq3(panes.windowId, input.id)).all();
1029
+ const streamIds = windowPanes.map((p2) => p2.eventStream?.streamId ?? null).filter((id) => id !== null);
1030
+ const streamsData = await amux.getStreams(streamIds);
1031
+ const streamMap = new Map(streamsData.map((s) => [s.id, s]));
446
1032
  return {
447
1033
  ...window,
448
1034
  panes: windowPanes.map((p2) => ({
449
1035
  ...p2,
450
- session: p2.content?.type === "session" ? sessionMap.get(p2.content.sessionId) ?? null : null
1036
+ stream: p2.eventStream ? streamMap.get(p2.eventStream.streamId) ?? null : null
451
1037
  }))
452
1038
  };
453
1039
  }),
454
1040
  /**
455
1041
  * Create a new window with a single pane.
456
- * If agentConfigId is not provided, pane is created with no content (shows picker).
1042
+ * If streamConfigId is not provided, pane is created with no content (shows picker).
457
1043
  */
458
1044
  create: publicProcedure.input(z.object({
459
1045
  id: z.string().optional(),
460
1046
  title: z.string().optional(),
461
1047
  directory: z.string().optional(),
1048
+ streamConfigId: z.string().optional(),
1049
+ // Legacy alias
462
1050
  agentConfigId: z.string().optional()
463
1051
  })).mutation(async ({ input }) => {
464
1052
  const directory = input.directory ?? process.cwd();
465
- let session = null;
466
- if (input.agentConfigId) {
467
- session = await amux.createSession({
1053
+ const configId = input.streamConfigId ?? input.agentConfigId;
1054
+ let stream = null;
1055
+ if (configId) {
1056
+ stream = await amux.createStream({
468
1057
  directory,
469
- agentConfigId: input.agentConfigId
1058
+ streamConfigId: configId
470
1059
  });
471
1060
  }
472
- const windowId = input.id ?? randomUUID2();
473
- const paneId = randomUUID2();
1061
+ const windowId = input.id ?? randomUUID3();
1062
+ const paneId = randomUUID3();
474
1063
  const existingCount = db.select().from(windows).all().length;
475
1064
  const layout = createLeaf(paneId);
476
1065
  db.insert(windows).values({
@@ -483,16 +1072,17 @@ var windowsRouter = router({
483
1072
  db.insert(panes).values({
484
1073
  id: paneId,
485
1074
  windowId,
486
- content: session ? { type: "session", sessionId: session.id } : null
1075
+ content: null,
1076
+ eventStream: stream ? { type: stream.streamType, streamId: stream.id } : null
487
1077
  }).run();
488
1078
  db.insert(uiState).values({ key: "active_window_id", value: windowId }).onConflictDoUpdate({ target: uiState.key, set: { value: windowId } }).run();
489
- const newWindow = db.select().from(windows).where(eq2(windows.id, windowId)).get();
490
- const newPanes = db.select().from(panes).where(eq2(panes.windowId, windowId)).all();
1079
+ const newWindow = db.select().from(windows).where(eq3(windows.id, windowId)).get();
1080
+ const newPanes = db.select().from(panes).where(eq3(panes.windowId, windowId)).all();
491
1081
  return {
492
1082
  ...newWindow,
493
1083
  panes: newPanes.map((p2) => ({
494
1084
  ...p2,
495
- session: session && p2.content?.type === "session" && p2.content.sessionId === session.id ? session : null
1085
+ stream: stream && p2.eventStream?.streamId === stream.id ? stream : null
496
1086
  }))
497
1087
  };
498
1088
  }),
@@ -509,28 +1099,28 @@ var windowsRouter = router({
509
1099
  updates.hasCustomTitle = true;
510
1100
  }
511
1101
  updates.lastAccessedAt = /* @__PURE__ */ new Date();
512
- db.update(windows).set(updates).where(eq2(windows.id, input.id)).run();
513
- return db.select().from(windows).where(eq2(windows.id, input.id)).get();
1102
+ db.update(windows).set(updates).where(eq3(windows.id, input.id)).run();
1103
+ return db.select().from(windows).where(eq3(windows.id, input.id)).get();
514
1104
  }),
515
1105
  /**
516
- * Delete a window and all its panes/sessions.
1106
+ * Delete a window and all its panes/streams.
517
1107
  */
518
1108
  delete: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
519
- const windowPanes = db.select().from(panes).where(eq2(panes.windowId, input.id)).all();
1109
+ const windowPanes = db.select().from(panes).where(eq3(panes.windowId, input.id)).all();
520
1110
  for (const pane of windowPanes) {
521
- if (pane.content?.type === "session") {
522
- await amux.deleteSession(pane.content.sessionId);
1111
+ if (pane.eventStream) {
1112
+ await amux.deleteStream(pane.eventStream.streamId);
523
1113
  }
524
1114
  }
525
- db.delete(windows).where(eq2(windows.id, input.id)).run();
526
- const activeRow = db.select().from(uiState).where(eq2(uiState.key, "active_window_id")).get();
1115
+ db.delete(windows).where(eq3(windows.id, input.id)).run();
1116
+ const activeRow = db.select().from(uiState).where(eq3(uiState.key, "active_window_id")).get();
527
1117
  if (activeRow?.value === input.id) {
528
1118
  const remaining = db.select().from(windows).orderBy(windows.createdAt).all();
529
1119
  const newActiveId = remaining[remaining.length - 1]?.id ?? null;
530
1120
  if (newActiveId) {
531
- db.update(uiState).set({ value: newActiveId }).where(eq2(uiState.key, "active_window_id")).run();
1121
+ db.update(uiState).set({ value: newActiveId }).where(eq3(uiState.key, "active_window_id")).run();
532
1122
  } else {
533
- db.delete(uiState).where(eq2(uiState.key, "active_window_id")).run();
1123
+ db.delete(uiState).where(eq3(uiState.key, "active_window_id")).run();
534
1124
  }
535
1125
  }
536
1126
  return { ok: true };
@@ -540,15 +1130,26 @@ var windowsRouter = router({
540
1130
  */
541
1131
  setActive: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
542
1132
  db.insert(uiState).values({ key: "active_window_id", value: input.id }).onConflictDoUpdate({ target: uiState.key, set: { value: input.id } }).run();
543
- db.update(windows).set({ lastAccessedAt: /* @__PURE__ */ new Date() }).where(eq2(windows.id, input.id)).run();
1133
+ db.update(windows).set({ lastAccessedAt: /* @__PURE__ */ new Date() }).where(eq3(windows.id, input.id)).run();
544
1134
  return { ok: true };
545
1135
  })
546
1136
  });
547
1137
 
548
1138
  // src/trpc/layout.ts
549
1139
  import { z as z2 } from "zod";
550
- import { randomUUID as randomUUID3 } from "crypto";
551
- import { eq as eq3 } from "drizzle-orm";
1140
+ import { randomUUID as randomUUID4 } from "crypto";
1141
+ import * as path6 from "path";
1142
+ import * as os3 from "os";
1143
+ import { eq as eq4 } from "drizzle-orm";
1144
+ function expandTilde(p2) {
1145
+ if (p2.startsWith("~/")) {
1146
+ return path6.join(os3.homedir(), p2.slice(2));
1147
+ }
1148
+ if (p2 === "~") {
1149
+ return os3.homedir();
1150
+ }
1151
+ return p2;
1152
+ }
552
1153
  var amux2;
553
1154
  function setAmuxBridge2(bridge) {
554
1155
  amux2 = bridge;
@@ -566,20 +1167,20 @@ var layoutRouter = router({
566
1167
  newPaneId: z2.string().optional(),
567
1168
  newSessionId: z2.string().optional()
568
1169
  })).mutation(async ({ input }) => {
569
- const sourcePane = db.select().from(panes).where(eq3(panes.id, input.paneId)).get();
1170
+ const sourcePane = db.select().from(panes).where(eq4(panes.id, input.paneId)).get();
570
1171
  if (!sourcePane) {
571
1172
  throw new Error(`Pane ${input.paneId} not found`);
572
1173
  }
573
- const window = db.select().from(windows).where(eq3(windows.id, input.windowId)).get();
1174
+ const window = db.select().from(windows).where(eq4(windows.id, input.windowId)).get();
574
1175
  if (!window) {
575
1176
  throw new Error(`Window ${input.windowId} not found`);
576
1177
  }
577
- const newContent = null;
578
- const newPaneId = input.newPaneId ?? randomUUID3();
1178
+ const newPaneId = input.newPaneId ?? randomUUID4();
579
1179
  db.insert(panes).values({
580
1180
  id: newPaneId,
581
1181
  windowId: input.windowId,
582
- content: newContent
1182
+ content: null,
1183
+ eventStream: null
583
1184
  }).run();
584
1185
  const currentLayout = window.layout;
585
1186
  const newLayout = splitPane(currentLayout, input.paneId, newPaneId, input.direction);
@@ -587,19 +1188,22 @@ var layoutRouter = router({
587
1188
  layout: newLayout,
588
1189
  activePaneId: newPaneId,
589
1190
  lastAccessedAt: /* @__PURE__ */ new Date()
590
- }).where(eq3(windows.id, input.windowId)).run();
1191
+ }).where(eq4(windows.id, input.windowId)).run();
591
1192
  const now = /* @__PURE__ */ new Date();
592
1193
  return {
593
1194
  newPaneId,
594
- newSessionId: null,
1195
+ newStreamId: null,
595
1196
  layout: newLayout,
596
1197
  newPane: {
597
1198
  id: newPaneId,
598
1199
  windowId: input.windowId,
599
- content: newContent,
1200
+ content: null,
1201
+ eventStream: null,
600
1202
  createdAt: now.toISOString(),
601
- session: null
602
- }
1203
+ stream: null
1204
+ },
1205
+ // Legacy aliases
1206
+ newSessionId: null
603
1207
  };
604
1208
  }),
605
1209
  /**
@@ -609,27 +1213,27 @@ var layoutRouter = router({
609
1213
  windowId: z2.string(),
610
1214
  paneId: z2.string()
611
1215
  })).mutation(async ({ input }) => {
612
- const window = db.select().from(windows).where(eq3(windows.id, input.windowId)).get();
1216
+ const window = db.select().from(windows).where(eq4(windows.id, input.windowId)).get();
613
1217
  if (!window) {
614
1218
  throw new Error(`Window ${input.windowId} not found`);
615
1219
  }
616
- const pane = db.select().from(panes).where(eq3(panes.id, input.paneId)).get();
1220
+ const pane = db.select().from(panes).where(eq4(panes.id, input.paneId)).get();
617
1221
  if (!pane) {
618
1222
  throw new Error(`Pane ${input.paneId} not found`);
619
1223
  }
620
1224
  const currentLayout = window.layout;
621
1225
  const allPaneIds = getAllPaneIds(currentLayout);
622
1226
  if (allPaneIds.length === 1) {
623
- if (pane.content?.type === "session") {
624
- await amux2.deleteSession(pane.content.sessionId);
1227
+ if (pane.eventStream) {
1228
+ await amux2.deleteStream(pane.eventStream.streamId);
625
1229
  }
626
- db.delete(windows).where(eq3(windows.id, input.windowId)).run();
1230
+ db.delete(windows).where(eq4(windows.id, input.windowId)).run();
627
1231
  return { windowClosed: true, layout: null, activePaneId: null };
628
1232
  }
629
- if (pane.content?.type === "session") {
630
- await amux2.deleteSession(pane.content.sessionId);
1233
+ if (pane.eventStream) {
1234
+ await amux2.deleteStream(pane.eventStream.streamId);
631
1235
  }
632
- db.delete(panes).where(eq3(panes.id, input.paneId)).run();
1236
+ db.delete(panes).where(eq4(panes.id, input.paneId)).run();
633
1237
  const newLayout = removePane(currentLayout, input.paneId);
634
1238
  if (!newLayout) {
635
1239
  throw new Error("Failed to remove pane from layout");
@@ -639,7 +1243,7 @@ var layoutRouter = router({
639
1243
  layout: newLayout,
640
1244
  activePaneId: newActivePaneId,
641
1245
  lastAccessedAt: /* @__PURE__ */ new Date()
642
- }).where(eq3(windows.id, input.windowId)).run();
1246
+ }).where(eq4(windows.id, input.windowId)).run();
643
1247
  return {
644
1248
  windowClosed: false,
645
1249
  layout: newLayout,
@@ -656,7 +1260,7 @@ var layoutRouter = router({
656
1260
  db.update(windows).set({
657
1261
  activePaneId: input.paneId,
658
1262
  lastAccessedAt: /* @__PURE__ */ new Date()
659
- }).where(eq3(windows.id, input.windowId)).run();
1263
+ }).where(eq4(windows.id, input.windowId)).run();
660
1264
  return { ok: true };
661
1265
  }),
662
1266
  /**
@@ -667,7 +1271,7 @@ var layoutRouter = router({
667
1271
  currentPaneId: z2.string(),
668
1272
  direction: z2.enum(["left", "right", "up", "down"])
669
1273
  })).mutation(async ({ input }) => {
670
- const window = db.select().from(windows).where(eq3(windows.id, input.windowId)).get();
1274
+ const window = db.select().from(windows).where(eq4(windows.id, input.windowId)).get();
671
1275
  if (!window) {
672
1276
  throw new Error(`Window ${input.windowId} not found`);
673
1277
  }
@@ -677,7 +1281,7 @@ var layoutRouter = router({
677
1281
  db.update(windows).set({
678
1282
  activePaneId: adjacentPaneId,
679
1283
  lastAccessedAt: /* @__PURE__ */ new Date()
680
- }).where(eq3(windows.id, input.windowId)).run();
1284
+ }).where(eq4(windows.id, input.windowId)).run();
681
1285
  return { newActivePaneId: adjacentPaneId };
682
1286
  }
683
1287
  return { newActivePaneId: null };
@@ -690,93 +1294,110 @@ var layoutRouter = router({
690
1294
  path: z2.array(z2.enum(["first", "second"])),
691
1295
  ratio: z2.number().min(10).max(90)
692
1296
  })).mutation(async ({ input }) => {
693
- const window = db.select().from(windows).where(eq3(windows.id, input.windowId)).get();
1297
+ const window = db.select().from(windows).where(eq4(windows.id, input.windowId)).get();
694
1298
  if (!window) {
695
1299
  throw new Error(`Window ${input.windowId} not found`);
696
1300
  }
697
1301
  const currentLayout = window.layout;
698
1302
  const newLayout = updateRatio(currentLayout, input.path, input.ratio);
699
- db.update(windows).set({ layout: newLayout }).where(eq3(windows.id, input.windowId)).run();
1303
+ db.update(windows).set({ layout: newLayout }).where(eq4(windows.id, input.windowId)).run();
700
1304
  return { layout: newLayout };
701
1305
  }),
702
1306
  /**
703
- * Set the session for an empty pane (used when user picks agent/shell type).
704
- * Creates a session in amux and updates the pane.
1307
+ * Set static content for a pane (browser URL, etc.).
1308
+ * Upsert behavior - works for both initial set and updates.
705
1309
  */
706
- setSession: publicProcedure.input(z2.object({
1310
+ setPaneContent: publicProcedure.input(z2.object({
707
1311
  paneId: z2.string(),
708
- agentConfigId: z2.string(),
709
- directory: z2.string().optional()
1312
+ content: z2.record(z2.unknown()).nullable(),
1313
+ rendererId: z2.string().optional()
710
1314
  })).mutation(async ({ input }) => {
711
- const pane = db.select().from(panes).where(eq3(panes.id, input.paneId)).get();
1315
+ const pane = db.select().from(panes).where(eq4(panes.id, input.paneId)).get();
712
1316
  if (!pane) {
713
1317
  throw new Error(`Pane ${input.paneId} not found`);
714
1318
  }
715
- if (pane.content) {
716
- throw new Error(`Pane ${input.paneId} already has content`);
717
- }
718
- const session = await amux2.createSession({
719
- directory: input.directory ?? process.cwd(),
720
- agentConfigId: input.agentConfigId
721
- });
722
- db.update(panes).set({ content: { type: "session", sessionId: session.id } }).where(eq3(panes.id, input.paneId)).run();
723
- await amux2.startAgent(session.id);
724
- return {
725
- sessionId: session.id,
726
- content: { type: "session", sessionId: session.id },
727
- session: {
728
- id: session.id,
729
- directory: session.directory,
730
- agentConfigId: session.agentConfigId,
731
- model: session.model,
732
- mode: session.mode,
733
- createdAt: session.createdAt.toISOString(),
734
- acpSessionId: session.acpSessionId
735
- }
736
- };
737
- }),
738
- /**
739
- * Set browser content for an empty pane.
740
- */
741
- setBrowserContent: publicProcedure.input(z2.object({
742
- paneId: z2.string(),
743
- url: z2.string().url()
744
- })).mutation(async ({ input }) => {
745
- const pane = db.select().from(panes).where(eq3(panes.id, input.paneId)).get();
746
- if (!pane) {
747
- throw new Error(`Pane ${input.paneId} not found`);
748
- }
749
- if (pane.content) {
750
- throw new Error(`Pane ${input.paneId} already has content`);
751
- }
752
- const content = { type: "browser", url: input.url };
753
- db.update(panes).set({ content }).where(eq3(panes.id, input.paneId)).run();
754
- return { content };
1319
+ db.update(panes).set({
1320
+ content: input.content,
1321
+ rendererId: input.rendererId ?? null
1322
+ }).where(eq4(panes.id, input.paneId)).run();
1323
+ return { content: input.content, rendererId: input.rendererId };
755
1324
  }),
756
1325
  /**
757
- * Update the URL of an existing browser pane.
1326
+ * Set an event stream for a pane (agent or shell).
1327
+ * Creates stream in amux and starts the backend.
758
1328
  */
759
- updateBrowserUrl: publicProcedure.input(z2.object({
1329
+ setPaneEventStream: publicProcedure.input(z2.object({
760
1330
  paneId: z2.string(),
761
- url: z2.string().url()
1331
+ streamConfigId: z2.string(),
1332
+ directory: z2.string().optional(),
1333
+ rendererId: z2.string().optional(),
1334
+ // Legacy alias
1335
+ agentConfigId: z2.string().optional()
762
1336
  })).mutation(async ({ input }) => {
763
- const pane = db.select().from(panes).where(eq3(panes.id, input.paneId)).get();
1337
+ const pane = db.select().from(panes).where(eq4(panes.id, input.paneId)).get();
764
1338
  if (!pane) {
765
1339
  throw new Error(`Pane ${input.paneId} not found`);
766
1340
  }
767
- if (pane.content?.type !== "browser") {
768
- throw new Error(`Pane ${input.paneId} is not a browser pane`);
1341
+ if (pane.eventStream) {
1342
+ throw new Error(`Pane ${input.paneId} already has an event stream`);
769
1343
  }
770
- const content = { type: "browser", url: input.url };
771
- db.update(panes).set({ content }).where(eq3(panes.id, input.paneId)).run();
772
- return { content };
1344
+ const configId = input.streamConfigId ?? input.agentConfigId;
1345
+ const directory = input.directory ? expandTilde(input.directory) : process.cwd();
1346
+ const stream = await amux2.createStream({
1347
+ directory,
1348
+ streamConfigId: configId
1349
+ });
1350
+ const eventStream = { type: stream.streamType, streamId: stream.id };
1351
+ db.update(panes).set({
1352
+ eventStream,
1353
+ rendererId: input.rendererId ?? null
1354
+ }).where(eq4(panes.id, input.paneId)).run();
1355
+ await amux2.startStream(stream.id);
1356
+ return {
1357
+ streamId: stream.id,
1358
+ eventStream,
1359
+ stream: {
1360
+ id: stream.id,
1361
+ directory: stream.directory,
1362
+ streamConfigId: stream.streamConfigId,
1363
+ streamType: stream.streamType,
1364
+ model: stream.model,
1365
+ mode: stream.mode,
1366
+ createdAt: stream.createdAt.toISOString()
1367
+ },
1368
+ // Legacy aliases
1369
+ sessionId: stream.id,
1370
+ process: eventStream,
1371
+ session: {
1372
+ id: stream.id,
1373
+ directory: stream.directory,
1374
+ agentConfigId: stream.streamConfigId,
1375
+ model: stream.model,
1376
+ mode: stream.mode,
1377
+ createdAt: stream.createdAt.toISOString()
1378
+ }
1379
+ };
773
1380
  })
774
1381
  });
775
1382
 
776
1383
  // src/trpc/agents.ts
777
1384
  import { z as z3 } from "zod";
778
- import { eq as eq4 } from "drizzle-orm";
779
1385
  import { observable } from "@trpc/server/observable";
1386
+
1387
+ // src/lib/panes.ts
1388
+ import { eq as eq5 } from "drizzle-orm";
1389
+ function getStreamIdForPane(paneId) {
1390
+ const pane = db.select().from(panes).where(eq5(panes.id, paneId)).get();
1391
+ if (!pane) {
1392
+ throw new Error(`Pane ${paneId} not found`);
1393
+ }
1394
+ if (!pane.eventStream) {
1395
+ throw new Error(`Pane ${paneId} does not have an event stream`);
1396
+ }
1397
+ return pane.eventStream.streamId;
1398
+ }
1399
+
1400
+ // src/trpc/agents.ts
780
1401
  var amux3;
781
1402
  function setAmuxBridge3(bridge) {
782
1403
  amux3 = bridge;
@@ -784,71 +1405,61 @@ function setAmuxBridge3(bridge) {
784
1405
  var paneInput = z3.object({
785
1406
  paneId: z3.string()
786
1407
  });
787
- function getSessionIdForPane(paneId) {
788
- const pane = db.select().from(panes).where(eq4(panes.id, paneId)).get();
789
- if (!pane) {
790
- throw new Error(`Pane ${paneId} not found`);
791
- }
792
- if (pane.content?.type !== "session") {
793
- throw new Error(`Pane ${paneId} is not a session pane`);
794
- }
795
- return pane.content.sessionId;
796
- }
797
- var agentsRouter = router({
1408
+ var streamsRouter = router({
798
1409
  /**
799
- * Start agent for a pane.
1410
+ * Start stream for a pane.
800
1411
  */
801
1412
  start: publicProcedure.input(paneInput).mutation(async ({ input }) => {
802
- const sessionId = getSessionIdForPane(input.paneId);
803
- return amux3.startAgent(sessionId);
1413
+ const streamId = getStreamIdForPane(input.paneId);
1414
+ return amux3.startStream(streamId);
804
1415
  }),
805
1416
  /**
806
- * Stop agent for a pane.
1417
+ * Stop stream for a pane.
807
1418
  */
808
1419
  stop: publicProcedure.input(paneInput).mutation(async ({ input }) => {
809
- const sessionId = getSessionIdForPane(input.paneId);
810
- await amux3.stopAgent(sessionId);
1420
+ const streamId = getStreamIdForPane(input.paneId);
1421
+ await amux3.stopStream(streamId);
811
1422
  return { ok: true };
812
1423
  }),
813
1424
  /**
814
- * Send a prompt to a pane's agent.
1425
+ * Send input to a pane's stream.
815
1426
  */
816
- prompt: publicProcedure.input(z3.object({
1427
+ input: publicProcedure.input(z3.object({
817
1428
  paneId: z3.string(),
818
1429
  message: z3.string()
819
1430
  })).mutation(async ({ input }) => {
820
- const sessionId = getSessionIdForPane(input.paneId);
821
- await amux3.prompt(sessionId, input.message);
1431
+ const streamId = getStreamIdForPane(input.paneId);
1432
+ await amux3.input(streamId, input.message);
822
1433
  return { ok: true };
823
1434
  }),
824
1435
  /**
825
1436
  * Cancel the current prompt.
826
1437
  */
827
1438
  cancel: publicProcedure.input(paneInput).mutation(async ({ input }) => {
828
- const sessionId = getSessionIdForPane(input.paneId);
829
- await amux3.cancel(sessionId);
1439
+ const streamId = getStreamIdForPane(input.paneId);
1440
+ await amux3.cancel(streamId);
830
1441
  return { ok: true };
831
1442
  }),
832
1443
  /**
833
- * Set agent mode.
1444
+ * Set stream mode.
834
1445
  */
835
1446
  setMode: publicProcedure.input(z3.object({
836
1447
  paneId: z3.string(),
837
1448
  modeId: z3.string()
838
1449
  })).mutation(async ({ input }) => {
839
- const sessionId = getSessionIdForPane(input.paneId);
840
- await amux3.setMode(sessionId, input.modeId);
1450
+ const streamId = getStreamIdForPane(input.paneId);
1451
+ await amux3.setMode(streamId, input.modeId);
841
1452
  return { ok: true };
842
1453
  }),
843
1454
  /**
844
- * Set agent model.
1455
+ * Set stream model.
845
1456
  */
846
1457
  setModel: publicProcedure.input(z3.object({
847
1458
  paneId: z3.string(),
848
1459
  modelId: z3.string()
849
1460
  })).mutation(async ({ input }) => {
850
- const sessionId = getSessionIdForPane(input.paneId);
851
- await amux3.setModel(sessionId, input.modelId);
1461
+ const streamId = getStreamIdForPane(input.paneId);
1462
+ await amux3.setModel(streamId, input.modelId);
852
1463
  return { ok: true };
853
1464
  }),
854
1465
  /**
@@ -859,17 +1470,17 @@ var agentsRouter = router({
859
1470
  requestId: z3.string(),
860
1471
  optionId: z3.string()
861
1472
  })).mutation(async ({ input }) => {
862
- const sessionId = getSessionIdForPane(input.paneId);
863
- amux3.respondPermission(sessionId, input.requestId, input.optionId);
1473
+ const streamId = getStreamIdForPane(input.paneId);
1474
+ amux3.respondPermission(streamId, input.requestId, input.optionId);
864
1475
  return { ok: true };
865
1476
  }),
866
1477
  /**
867
- * Subscribe to updates for a pane's agent session.
1478
+ * Subscribe to updates for a pane's stream.
868
1479
  */
869
1480
  subscribe: publicProcedure.input(paneInput).subscription(({ input }) => {
870
- const sessionId = getSessionIdForPane(input.paneId);
1481
+ const streamId = getStreamIdForPane(input.paneId);
871
1482
  return observable((emit) => {
872
- return amux3.subscribeToSession(sessionId, (update) => {
1483
+ return amux3.subscribeToStream(streamId, (update) => {
873
1484
  emit.next(update);
874
1485
  });
875
1486
  });
@@ -878,22 +1489,42 @@ var agentsRouter = router({
878
1489
 
879
1490
  // src/trpc/files.ts
880
1491
  import { z as z4 } from "zod";
881
- import { eq as eq5 } from "drizzle-orm";
1492
+ import * as fs6 from "fs/promises";
1493
+ import * as path7 from "path";
1494
+ import * as os4 from "os";
1495
+ function expandTilde2(p2) {
1496
+ if (p2.startsWith("~/")) {
1497
+ return path7.join(os4.homedir(), p2.slice(2));
1498
+ }
1499
+ if (p2 === "~") {
1500
+ return os4.homedir();
1501
+ }
1502
+ return p2;
1503
+ }
882
1504
  var amux4;
883
1505
  function setAmuxBridge4(bridge) {
884
1506
  amux4 = bridge;
885
1507
  }
886
- function getSessionIdForPane2(paneId) {
887
- const pane = db.select().from(panes).where(eq5(panes.id, paneId)).get();
888
- if (!pane) {
889
- throw new Error(`Pane ${paneId} not found`);
890
- }
891
- if (pane.content?.type !== "session") {
892
- throw new Error(`Pane ${paneId} is not a session pane`);
893
- }
894
- return pane.content.sessionId;
895
- }
896
1508
  var filesRouter = router({
1509
+ /**
1510
+ * Get server's current working directory.
1511
+ */
1512
+ getCwd: publicProcedure.query(() => process.cwd()),
1513
+ /**
1514
+ * List directories in a given path.
1515
+ * Used by DirectoryPicker for browsing.
1516
+ */
1517
+ listDirectory: publicProcedure.input(z4.object({
1518
+ directory: z4.string()
1519
+ })).query(async ({ input }) => {
1520
+ try {
1521
+ const absPath = path7.resolve(expandTilde2(input.directory));
1522
+ const entries = await fs6.readdir(absPath, { withFileTypes: true });
1523
+ return entries.filter((e) => e.isDirectory()).map((e) => ({ name: e.name, isDirectory: true })).sort((a, b) => a.name.localeCompare(b.name));
1524
+ } catch {
1525
+ return [];
1526
+ }
1527
+ }),
897
1528
  /**
898
1529
  * List files/directories matching a partial path for autocomplete.
899
1530
  * Returns files in the session's working directory.
@@ -903,8 +1534,8 @@ var filesRouter = router({
903
1534
  partialPath: z4.string(),
904
1535
  limit: z4.number().default(20)
905
1536
  })).query(async ({ input }) => {
906
- const sessionId = getSessionIdForPane2(input.paneId);
907
- return amux4.listFilesForAutocomplete(sessionId, input.partialPath, input.limit);
1537
+ const streamId = getStreamIdForPane(input.paneId);
1538
+ return amux4.listFilesForAutocomplete(streamId, input.partialPath, input.limit);
908
1539
  })
909
1540
  });
910
1541
 
@@ -918,15 +1549,15 @@ function setAmuxBridge5(bridge) {
918
1549
  var paneInput2 = z5.object({
919
1550
  paneId: z5.string()
920
1551
  });
921
- function getSessionIdForPane3(paneId) {
1552
+ function getStreamIdForPane2(paneId) {
922
1553
  const pane = db.select().from(panes).where(eq6(panes.id, paneId)).get();
923
1554
  if (!pane) {
924
1555
  throw new Error(`Pane ${paneId} not found`);
925
1556
  }
926
- if (pane.content?.type !== "session") {
927
- throw new Error(`Pane ${paneId} is not a session pane`);
1557
+ if (!pane.eventStream) {
1558
+ throw new Error(`Pane ${paneId} does not have an event stream`);
928
1559
  }
929
- return pane.content.sessionId;
1560
+ return pane.eventStream.streamId;
930
1561
  }
931
1562
  var terminalsRouter = router({
932
1563
  /**
@@ -938,8 +1569,8 @@ var terminalsRouter = router({
938
1569
  data: z5.string()
939
1570
  })
940
1571
  ).mutation(({ input }) => {
941
- const sessionId = getSessionIdForPane3(input.paneId);
942
- amux5.terminalWrite(sessionId, input.data);
1572
+ const streamId = getStreamIdForPane2(input.paneId);
1573
+ amux5.terminalWrite(streamId, input.data);
943
1574
  return { ok: true };
944
1575
  }),
945
1576
  /**
@@ -952,16 +1583,16 @@ var terminalsRouter = router({
952
1583
  rows: z5.number().int().positive()
953
1584
  })
954
1585
  ).mutation(({ input }) => {
955
- const sessionId = getSessionIdForPane3(input.paneId);
956
- amux5.terminalResize(sessionId, input.cols, input.rows);
1586
+ const streamId = getStreamIdForPane2(input.paneId);
1587
+ amux5.terminalResize(streamId, input.cols, input.rows);
957
1588
  return { ok: true };
958
1589
  }),
959
1590
  /**
960
- * Check if a pane is a terminal session.
1591
+ * Check if a pane is a terminal stream.
961
1592
  */
962
1593
  isTerminal: publicProcedure.input(paneInput2).query(({ input }) => {
963
- const sessionId = getSessionIdForPane3(input.paneId);
964
- return { isTerminal: amux5.isTerminalSession(sessionId) };
1594
+ const streamId = getStreamIdForPane2(input.paneId);
1595
+ return { isTerminal: amux5.isTerminalStream(streamId) };
965
1596
  }),
966
1597
  /**
967
1598
  * Set terminal title (from OSC escape sequence).
@@ -970,12 +1601,264 @@ var terminalsRouter = router({
970
1601
  paneId: z5.string(),
971
1602
  title: z5.string()
972
1603
  })).mutation(({ input }) => {
973
- const sessionId = getSessionIdForPane3(input.paneId);
974
- amux5.updateSessionTitle(sessionId, input.title);
1604
+ const streamId = getStreamIdForPane2(input.paneId);
1605
+ amux5.updateStreamTitle(streamId, input.title);
975
1606
  return { ok: true };
976
1607
  })
977
1608
  });
978
1609
 
1610
+ // src/trpc/renderers.ts
1611
+ import { observable as observable2 } from "@trpc/server/observable";
1612
+
1613
+ // src/lib/rendererWatcher.ts
1614
+ import chokidar2 from "chokidar";
1615
+ import * as path9 from "path";
1616
+ import * as os5 from "os";
1617
+ import * as fs8 from "fs/promises";
1618
+ import { EventEmitter as EventEmitter2 } from "events";
1619
+
1620
+ // src/lib/rendererCompiler.ts
1621
+ import { build as build2 } from "esbuild";
1622
+ import * as fs7 from "fs/promises";
1623
+ import * as path8 from "path";
1624
+ async function compileRenderer(pluginDir) {
1625
+ const metaPath = path8.join(pluginDir, "meta.json");
1626
+ const entryPath = path8.join(pluginDir, "index.tsx");
1627
+ const pkgPath = path8.join(pluginDir, "package.json");
1628
+ let meta;
1629
+ try {
1630
+ const metaContent = await fs7.readFile(metaPath, "utf-8");
1631
+ meta = JSON.parse(metaContent);
1632
+ } catch (err) {
1633
+ throw new Error(
1634
+ `Failed to read meta file for ${pluginDir}. Expected ${metaPath} with { name, target } properties. Error: ${err instanceof Error ? err.message : String(err)}`
1635
+ );
1636
+ }
1637
+ if (!meta.name || typeof meta.name !== "string") {
1638
+ throw new Error(`Meta file ${metaPath} must have a 'name' string property`);
1639
+ }
1640
+ if (!meta.target || !meta.target.kind) {
1641
+ throw new Error(`Meta file ${metaPath} must have a 'target' object with 'kind' property`);
1642
+ }
1643
+ let hasPackageJson = false;
1644
+ try {
1645
+ const pkgContent = await fs7.readFile(pkgPath, "utf-8");
1646
+ const pkg = JSON.parse(pkgContent);
1647
+ hasPackageJson = true;
1648
+ const deps = pkg.dependencies || {};
1649
+ if (deps["react"] || deps["react-dom"]) {
1650
+ throw new Error(
1651
+ `Plugin "${meta.name}": react and react-dom must be in peerDependencies, not dependencies. This ensures plugins share the same React instance as the host app.`
1652
+ );
1653
+ }
1654
+ } catch (err) {
1655
+ if (err instanceof Error && err.message.includes("peerDependencies")) {
1656
+ throw err;
1657
+ }
1658
+ }
1659
+ const result = await build2({
1660
+ entryPoints: [entryPath],
1661
+ bundle: true,
1662
+ write: false,
1663
+ format: "esm",
1664
+ jsx: "automatic",
1665
+ target: "es2020",
1666
+ external: ["react", "react-dom", "react/jsx-runtime", "@bytespell/shella-sdk", "@bytespell/shella-sdk/components/ui"],
1667
+ // If plugin has package.json, look for node_modules
1668
+ nodePaths: hasPackageJson ? [path8.join(pluginDir, "node_modules")] : [],
1669
+ minify: false
1670
+ });
1671
+ const code = result.outputFiles[0].text;
1672
+ const id = `user:${path8.basename(pluginDir)}`;
1673
+ return {
1674
+ id,
1675
+ name: meta.name,
1676
+ description: meta.description,
1677
+ target: meta.target,
1678
+ code
1679
+ };
1680
+ }
1681
+ async function isValidPlugin(dirPath) {
1682
+ try {
1683
+ await fs7.access(path8.join(dirPath, "meta.json"));
1684
+ await fs7.access(path8.join(dirPath, "index.tsx"));
1685
+ return true;
1686
+ } catch {
1687
+ return false;
1688
+ }
1689
+ }
1690
+
1691
+ // src/lib/rendererWatcher.ts
1692
+ var RendererEventEmitter = class extends EventEmitter2 {
1693
+ emit(event, data) {
1694
+ return super.emit(event, data);
1695
+ }
1696
+ on(event, listener) {
1697
+ return super.on(event, listener);
1698
+ }
1699
+ off(event, listener) {
1700
+ return super.off(event, listener);
1701
+ }
1702
+ };
1703
+ function watchRenderers(emitter) {
1704
+ const userPath = path9.join(os5.homedir(), ".shella", "renderers");
1705
+ ensureDir2(userPath);
1706
+ const knownPlugins = /* @__PURE__ */ new Map();
1707
+ const pendingCompiles = /* @__PURE__ */ new Map();
1708
+ const watcher = chokidar2.watch(userPath, {
1709
+ ignoreInitial: false,
1710
+ persistent: true,
1711
+ awaitWriteFinish: {
1712
+ stabilityThreshold: 100,
1713
+ pollInterval: 50
1714
+ },
1715
+ depth: 3
1716
+ // Watch inside plugin folders for nested components
1717
+ });
1718
+ const getPluginDir = (filePath) => {
1719
+ const relative3 = path9.relative(userPath, filePath);
1720
+ const parts = relative3.split(path9.sep);
1721
+ if (parts.length >= 1 && parts[0]) {
1722
+ return path9.join(userPath, parts[0]);
1723
+ }
1724
+ return null;
1725
+ };
1726
+ const scheduleCompile = (pluginDir) => {
1727
+ if (pendingCompiles.has(pluginDir)) {
1728
+ clearTimeout(pendingCompiles.get(pluginDir));
1729
+ }
1730
+ pendingCompiles.set(
1731
+ pluginDir,
1732
+ setTimeout(async () => {
1733
+ pendingCompiles.delete(pluginDir);
1734
+ await handlePluginChange(pluginDir);
1735
+ }, 100)
1736
+ );
1737
+ };
1738
+ const handlePluginChange = async (pluginDir) => {
1739
+ if (!await isValidPlugin(pluginDir)) return;
1740
+ try {
1741
+ const compiled = await compileRenderer(pluginDir);
1742
+ if (!knownPlugins.has(pluginDir)) {
1743
+ knownPlugins.set(pluginDir, true);
1744
+ console.log(`[renderers] Added: ${compiled.id}`);
1745
+ emitter.emit("renderer:added", compiled);
1746
+ } else {
1747
+ console.log(`[renderers] Updated: ${compiled.id}`);
1748
+ emitter.emit("renderer:updated", { id: compiled.id, code: compiled.code });
1749
+ }
1750
+ } catch (err) {
1751
+ console.error(`[renderers] Error compiling ${pluginDir}:`, err);
1752
+ emitter.emit("renderer:error", {
1753
+ filePath: pluginDir,
1754
+ error: err instanceof Error ? err : new Error(String(err))
1755
+ });
1756
+ }
1757
+ };
1758
+ watcher.on("add", (filePath) => {
1759
+ const pluginDir = getPluginDir(filePath);
1760
+ if (pluginDir) scheduleCompile(pluginDir);
1761
+ });
1762
+ watcher.on("change", (filePath) => {
1763
+ const pluginDir = getPluginDir(filePath);
1764
+ if (pluginDir) scheduleCompile(pluginDir);
1765
+ });
1766
+ watcher.on("unlinkDir", (dirPath) => {
1767
+ if (knownPlugins.has(dirPath)) {
1768
+ const id = `user:${path9.basename(dirPath)}`;
1769
+ knownPlugins.delete(dirPath);
1770
+ console.log(`[renderers] Removed: ${id}`);
1771
+ emitter.emit("renderer:removed", { id });
1772
+ }
1773
+ });
1774
+ watcher.on("error", (err) => {
1775
+ console.error("[renderers] Watcher error:", err);
1776
+ });
1777
+ return () => {
1778
+ for (const timeout of pendingCompiles.values()) {
1779
+ clearTimeout(timeout);
1780
+ }
1781
+ pendingCompiles.clear();
1782
+ watcher.close();
1783
+ };
1784
+ }
1785
+ async function ensureDir2(dirPath) {
1786
+ try {
1787
+ await fs8.mkdir(dirPath, { recursive: true });
1788
+ } catch {
1789
+ }
1790
+ }
1791
+ async function scanRenderers() {
1792
+ const userPath = path9.join(os5.homedir(), ".shella", "renderers");
1793
+ const renderers = [];
1794
+ try {
1795
+ const entries = await fs8.readdir(userPath, { withFileTypes: true });
1796
+ for (const entry of entries) {
1797
+ if (!entry.isDirectory()) continue;
1798
+ const pluginDir = path9.join(userPath, entry.name);
1799
+ if (await isValidPlugin(pluginDir)) {
1800
+ try {
1801
+ const compiled = await compileRenderer(pluginDir);
1802
+ renderers.push(compiled);
1803
+ } catch (err) {
1804
+ console.error(`[renderers] Error compiling ${pluginDir}:`, err);
1805
+ }
1806
+ }
1807
+ }
1808
+ } catch {
1809
+ }
1810
+ return renderers;
1811
+ }
1812
+
1813
+ // src/trpc/renderers.ts
1814
+ var rendererEmitter = new RendererEventEmitter();
1815
+ var stopWatcher = null;
1816
+ function initRendererWatcher() {
1817
+ if (stopWatcher) {
1818
+ stopWatcher();
1819
+ }
1820
+ stopWatcher = watchRenderers(rendererEmitter);
1821
+ }
1822
+ var renderersRouter = router({
1823
+ /**
1824
+ * List all currently available user renderers
1825
+ * Used for initial hydration when client connects.
1826
+ */
1827
+ list: publicProcedure.query(async () => {
1828
+ return scanRenderers();
1829
+ }),
1830
+ /**
1831
+ * Subscribe to renderer additions
1832
+ */
1833
+ onAdded: publicProcedure.subscription(() => {
1834
+ return observable2((emit) => {
1835
+ const handler = (data) => emit.next(data);
1836
+ rendererEmitter.on("renderer:added", handler);
1837
+ return () => rendererEmitter.off("renderer:added", handler);
1838
+ });
1839
+ }),
1840
+ /**
1841
+ * Subscribe to renderer updates (code changes)
1842
+ */
1843
+ onUpdated: publicProcedure.subscription(() => {
1844
+ return observable2((emit) => {
1845
+ const handler = (data) => emit.next(data);
1846
+ rendererEmitter.on("renderer:updated", handler);
1847
+ return () => rendererEmitter.off("renderer:updated", handler);
1848
+ });
1849
+ }),
1850
+ /**
1851
+ * Subscribe to renderer removals
1852
+ */
1853
+ onRemoved: publicProcedure.subscription(() => {
1854
+ return observable2((emit) => {
1855
+ const handler = (data) => emit.next(data);
1856
+ rendererEmitter.on("renderer:removed", handler);
1857
+ return () => rendererEmitter.off("renderer:removed", handler);
1858
+ });
1859
+ })
1860
+ });
1861
+
979
1862
  // src/trpc/router.ts
980
1863
  function initializeRouters(amux6) {
981
1864
  setAmuxBridge(amux6);
@@ -983,39 +1866,60 @@ function initializeRouters(amux6) {
983
1866
  setAmuxBridge3(amux6);
984
1867
  setAmuxBridge4(amux6);
985
1868
  setAmuxBridge5(amux6);
1869
+ initRendererWatcher();
986
1870
  }
987
1871
  var shellaRouter = router({
988
1872
  windows: windowsRouter,
989
1873
  layout: layoutRouter,
990
- agents: agentsRouter,
1874
+ streams: streamsRouter,
991
1875
  files: filesRouter,
992
- terminals: terminalsRouter
1876
+ terminals: terminalsRouter,
1877
+ renderers: renderersRouter,
1878
+ // Legacy alias
1879
+ agents: streamsRouter
993
1880
  });
994
1881
 
995
1882
  // src/index.ts
996
- var __dirname = path2.dirname(fileURLToPath(import.meta.url));
1883
+ discoverAndRegisterConfigs(builtInDiscoverers);
1884
+ var __dirname2 = path10.dirname(fileURLToPath2(import.meta.url));
997
1885
  function createShellaServer(options = {}) {
998
1886
  const port = options.port ?? 3067;
999
1887
  if (options.verbose) {
1000
1888
  setVerbose(true);
1001
1889
  }
1890
+ const driverEmitter = new DriverEventEmitter();
1891
+ let stopDriverWatcher = null;
1892
+ driverEmitter.on("driver:added", ({ id, name }) => {
1893
+ debug("shella", `Driver registered: ${id} (${name})`);
1894
+ });
1895
+ driverEmitter.on("driver:updated", ({ id, name }) => {
1896
+ debug("shella", `Driver updated: ${id} (${name})`);
1897
+ });
1898
+ driverEmitter.on("driver:removed", ({ id }) => {
1899
+ debug("shella", `Driver removed: ${id}`);
1900
+ });
1901
+ driverEmitter.on("driver:error", ({ pluginDir, error }) => {
1902
+ console.error(`Driver error in ${pluginDir}:`, error.message);
1903
+ });
1002
1904
  const amuxBridge = createAmuxBridge();
1003
1905
  initializeRouters(amuxBridge);
1004
1906
  const app = express();
1005
1907
  app.use(express.json());
1006
- const publicPath = path2.join(__dirname, "..", "public");
1908
+ const publicPath = path10.join(__dirname2, "..", "public");
1007
1909
  app.use(express.static(publicPath));
1008
1910
  app.use("/trpc", createExpressMiddleware({ router: shellaRouter }));
1009
1911
  app.get("/health", (_req, res) => res.json({ status: "ok" }));
1010
1912
  app.get("/{*splat}", (_req, res) => {
1011
- res.sendFile(path2.join(publicPath, "index.html"));
1913
+ res.sendFile(path10.join(publicPath, "index.html"));
1012
1914
  });
1013
1915
  const server = createServer(app);
1014
1916
  const wss = new WebSocketServer({ server, path: "/trpc" });
1015
1917
  applyWSSHandler({ wss, router: shellaRouter });
1016
1918
  return {
1017
- start: () => {
1018
- return new Promise((resolve, reject) => {
1919
+ start: async () => {
1920
+ await scanDrivers();
1921
+ stopDriverWatcher = watchDrivers(driverEmitter);
1922
+ return new Promise((resolve4, reject) => {
1019
1923
  server.once("error", (err) => {
1020
1924
  if (err.code === "EADDRINUSE") {
1021
1925
  reject(new Error(`Port ${port} is already in use. Kill the other process or use a different port.`));
@@ -1027,14 +1931,15 @@ function createShellaServer(options = {}) {
1027
1931
  const url = `http://localhost:${port}`;
1028
1932
  debug("shella", `Running on ${url}`);
1029
1933
  options.onReady?.(url);
1030
- resolve();
1934
+ resolve4();
1031
1935
  });
1032
1936
  });
1033
1937
  },
1034
1938
  stop: async () => {
1035
1939
  debug("shella", "Shutting down...");
1940
+ stopDriverWatcher?.();
1036
1941
  wss.close();
1037
- await agentManager2.stopAll();
1942
+ await muxManager3.stopAll();
1038
1943
  server.close();
1039
1944
  }
1040
1945
  };