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