@soederpop/luca 0.0.28 → 0.0.30

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 (51) hide show
  1. package/commands/try-all-challenges.ts +1 -1
  2. package/docs/TABLE-OF-CONTENTS.md +0 -3
  3. package/docs/examples/structured-output-with-assistants.md +144 -0
  4. package/docs/tutorials/20-browser-esm.md +234 -0
  5. package/package.json +1 -1
  6. package/src/agi/container.server.ts +4 -0
  7. package/src/agi/features/assistant.ts +132 -2
  8. package/src/agi/features/browser-use.ts +623 -0
  9. package/src/agi/features/conversation.ts +135 -45
  10. package/src/agi/lib/interceptor-chain.ts +79 -0
  11. package/src/bootstrap/generated.ts +381 -308
  12. package/src/cli/build-info.ts +2 -2
  13. package/src/clients/rest.ts +7 -7
  14. package/src/commands/chat.ts +22 -0
  15. package/src/commands/describe.ts +67 -2
  16. package/src/commands/prompt.ts +23 -3
  17. package/src/container.ts +411 -113
  18. package/src/helper.ts +189 -5
  19. package/src/introspection/generated.agi.ts +17664 -11568
  20. package/src/introspection/generated.node.ts +4891 -1860
  21. package/src/introspection/generated.web.ts +379 -291
  22. package/src/introspection/index.ts +7 -0
  23. package/src/introspection/scan.ts +224 -7
  24. package/src/node/container.ts +31 -10
  25. package/src/node/features/content-db.ts +7 -7
  26. package/src/node/features/disk-cache.ts +11 -11
  27. package/src/node/features/esbuild.ts +3 -3
  28. package/src/node/features/file-manager.ts +37 -16
  29. package/src/node/features/fs.ts +64 -25
  30. package/src/node/features/git.ts +10 -10
  31. package/src/node/features/helpers.ts +25 -18
  32. package/src/node/features/ink.ts +13 -13
  33. package/src/node/features/ipc-socket.ts +8 -8
  34. package/src/node/features/networking.ts +3 -3
  35. package/src/node/features/os.ts +7 -7
  36. package/src/node/features/package-finder.ts +15 -15
  37. package/src/node/features/proc.ts +1 -1
  38. package/src/node/features/ui.ts +13 -13
  39. package/src/node/features/vm.ts +4 -4
  40. package/src/scaffolds/generated.ts +1 -1
  41. package/src/servers/express.ts +6 -6
  42. package/src/servers/mcp.ts +4 -4
  43. package/src/servers/socket.ts +6 -6
  44. package/test/interceptor-chain.test.ts +61 -0
  45. package/docs/apis/features/node/window-manager.md +0 -445
  46. package/docs/examples/window-manager-layouts.md +0 -180
  47. package/docs/examples/window-manager.md +0 -125
  48. package/docs/window-manager-fix.md +0 -249
  49. package/scripts/test-window-manager-lifecycle.ts +0 -86
  50. package/scripts/test-window-manager.ts +0 -43
  51. package/src/node/features/window-manager.ts +0 -1603
@@ -1,180 +0,0 @@
1
- ---
2
- title: "Window Manager Layouts"
3
- tags: [windowManager, layout, native, ipc, macos, multi-window]
4
- lastTested: null
5
- lastTestPassed: null
6
- ---
7
-
8
- # Window Manager Layouts
9
-
10
- Spawn and manage multiple windows at once using the layout API. Layouts let you declare groups of browser and terminal windows that open in parallel, or sequence multiple groups so each batch waits for the previous one to finish.
11
-
12
- ## Overview
13
-
14
- The `windowManager` feature exposes two layout methods:
15
-
16
- - **`spawnLayout(config)`** — spawns all entries in parallel, returns `WindowHandle[]`
17
- - **`spawnLayouts(configs)`** — spawns multiple layouts sequentially (each layout's windows still spawn in parallel), returns `WindowHandle[][]`
18
-
19
- Each entry in a layout is a `LayoutEntry` — either a browser window or a TTY window. Type detection is automatic: if the entry has a `command` field or `type: 'tty'`, it's a terminal. Otherwise it's a browser window.
20
-
21
- ## Setup
22
-
23
- ```ts
24
- const wm = container.feature('windowManager', {
25
- autoListen: true,
26
- requestTimeoutMs: 10000
27
- })
28
- console.log('Window Manager ready')
29
- ```
30
-
31
- ## Single Layout — Parallel Windows
32
-
33
- Spawn a mix of browser and terminal windows that all open at the same time.
34
-
35
- ```ts
36
- const handles = await wm.spawnLayout([
37
- { url: 'https://github.com', width: '50%', height: '100%', x: 0, y: 0 },
38
- { url: 'https://soederpop.com', width: '50%', height: '100%', x: '50%', y: 0 },
39
- { command: 'top', title: 'System Monitor', width: 900, height: 400, x: 0, y: 720 },
40
- ])
41
-
42
- console.log('Spawned', handles.length, 'windows')
43
- handles.forEach((h, i) => console.log(` [${i}] windowId: ${h.windowId}`))
44
- ```
45
-
46
- All three windows open simultaneously. The returned `handles` array preserves the same order as the config entries, so `handles[0]` corresponds to the GitHub window, `handles[1]` to HN, and `handles[2]` to htop.
47
-
48
- ## Percentage-Based Dimensions
49
-
50
- Dimensions (`width`, `height`, `x`, `y`) accept percentage strings resolved against the primary display. This makes layouts portable across different screen resolutions.
51
-
52
- ```ts skip
53
- // Side-by-side: two windows each taking half the screen
54
- const handles = await wm.spawnLayout([
55
- { url: 'https://github.com', width: '50%', height: '100%', x: '0%', y: '0%' },
56
- { url: 'https://news.ycombinator.com', width: '50%', height: '100%', x: '50%', y: '0%' },
57
- ])
58
- ```
59
-
60
- You can mix absolute and percentage values freely:
61
-
62
- ```ts skip
63
- const handles = await wm.spawnLayout([
64
- { url: 'https://example.com', width: '75%', height: 600, x: '12.5%', y: 50 },
65
- { command: 'htop', width: '100%', height: '30%', x: '0%', y: '70%' },
66
- ])
67
- ```
68
-
69
- ## Explicit Type Field
70
-
71
- You can be explicit about entry types using the `type` field. This is equivalent to the implicit detection but more readable when mixing window types.
72
-
73
- ```ts skip
74
- const handles = await wm.spawnLayout([
75
- { type: 'window', url: 'https://example.com', width: 800, height: 600 },
76
- { type: 'tty', command: 'tail -f /var/log/system.log', title: 'Logs' },
77
- ])
78
-
79
- console.log('Browser window:', handles[0].windowId)
80
- console.log('TTY window:', handles[1].windowId)
81
- ```
82
-
83
- ## Sequential Layouts
84
-
85
- Use `spawnLayouts()` when you need windows to appear in stages. Each layout batch spawns in parallel, but the next batch waits until the previous one is fully ready.
86
-
87
- ```ts skip
88
- const [dashboards, tools] = await wm.spawnLayouts([
89
- // First batch: main content
90
- [
91
- { url: 'https://grafana.internal/d/api-latency', width: 960, height: 800, x: 0, y: 0 },
92
- { url: 'https://grafana.internal/d/error-rate', width: 960, height: 800, x: 970, y: 0 },
93
- ],
94
- // Second batch: supporting tools (opens after dashboards are ready)
95
- [
96
- { command: 'htop', title: 'CPU', width: 640, height: 400, x: 0, y: 820 },
97
- { command: 'tail -f /var/log/system.log', title: 'Logs', width: 640, height: 400, x: 650, y: 820 },
98
- ],
99
- ])
100
-
101
- console.log('Dashboards:', dashboards.map(h => h.windowId))
102
- console.log('Tools:', tools.map(h => h.windowId))
103
- ```
104
-
105
- This is useful when the second batch depends on the first being visible — for example, positioning tool windows below dashboard windows.
106
-
107
- ## Lifecycle Events on Layout Handles
108
-
109
- Every handle returned from a layout supports the same event API as a single `spawn()` call. You can listen for `close` and `terminalExited` events on each handle independently.
110
-
111
- ```ts skip
112
- const handles = await wm.spawnLayout([
113
- { url: 'https://example.com', width: 800, height: 600 },
114
- { command: 'sleep 5 && echo done', title: 'Short Task' },
115
- ])
116
-
117
- const [browser, terminal] = handles
118
-
119
- browser.on('close', () => console.log('Browser window closed'))
120
-
121
- terminal.on('terminalExited', (info) => {
122
- console.log('Terminal process finished:', info)
123
- })
124
- ```
125
-
126
- ## Window Chrome Options
127
-
128
- Layout entries support all the same window chrome options as regular `spawn()` calls.
129
-
130
- ```ts skip
131
- const handles = await wm.spawnLayout([
132
- {
133
- url: 'https://example.com',
134
- width: 400,
135
- height: 300,
136
- alwaysOnTop: true,
137
- window: {
138
- decorations: 'hiddenTitleBar',
139
- transparent: true,
140
- shadow: true,
141
- opacity: 0.9,
142
- }
143
- },
144
- {
145
- url: 'https://example.com/overlay',
146
- width: 200,
147
- height: 100,
148
- window: {
149
- decorations: 'none',
150
- clickThrough: true,
151
- transparent: true,
152
- }
153
- },
154
- ])
155
- ```
156
-
157
- ## Operating on All Handles
158
-
159
- Since layouts return arrays of `WindowHandle`, you can easily batch operations across all windows.
160
-
161
- ```ts skip
162
- const handles = await wm.spawnLayout([
163
- { url: 'https://example.com/a', width: 600, height: 400 },
164
- { url: 'https://example.com/b', width: 600, height: 400 },
165
- { url: 'https://example.com/c', width: 600, height: 400 },
166
- ])
167
-
168
- // Navigate all windows to the same URL
169
- await Promise.all(handles.map(h => h.navigate('https://example.com/updated')))
170
-
171
- // Screenshot all windows
172
- await Promise.all(handles.map((h, i) => h.screengrab(`./layout-${i}.png`)))
173
-
174
- // Close all windows
175
- await Promise.all(handles.map(h => h.close()))
176
- ```
177
-
178
- ## Summary
179
-
180
- The layout API builds on top of `spawn()` and `spawnTTY()` to orchestrate multi-window setups. Use `spawnLayout()` for a single batch of parallel windows, and `spawnLayouts()` when you need staged sequences. Every returned handle supports the full `WindowHandle` API — events, navigation, eval, screenshots, and more.
@@ -1,125 +0,0 @@
1
- ---
2
- title: "Window Manager"
3
- tags: [windowManager, native, ipc, macos, browser, window]
4
- lastTested: null
5
- lastTestPassed: null
6
- ---
7
-
8
- # windowManager
9
-
10
- Native window control via LucaVoiceLauncher IPC. Communicates with the macOS launcher app over a Unix domain socket using NDJSON to spawn, navigate, screenshot, and manage native browser windows.
11
-
12
- ## Overview
13
-
14
- Use the `windowManager` feature when you need to control native browser windows from Luca. It acts as an IPC server that the LucaVoiceLauncher macOS app connects to. Through this connection you can spawn windows with configurable chrome, navigate URLs, evaluate JavaScript, capture screenshots, and record video.
15
-
16
- Requires the LucaVoiceLauncher native macOS app to be running and connected.
17
-
18
- ## Enabling the Feature
19
-
20
- ```ts
21
- const wm = container.feature('windowManager', {
22
- autoListen: false,
23
- requestTimeoutMs: 10000
24
- })
25
- console.log('Window Manager feature created')
26
- console.log('Listening:', wm.isListening)
27
- console.log('Client connected:', wm.isClientConnected)
28
- ```
29
-
30
- ## API Documentation
31
-
32
- ```ts
33
- const info = await container.features.describe('windowManager')
34
- console.log(info)
35
- ```
36
-
37
- ## Spawning Windows
38
-
39
- Create native browser windows with configurable dimensions and chrome.
40
-
41
- ```ts skip
42
- const result = await wm.spawn({
43
- url: 'https://google.com',
44
- width: 1024,
45
- height: 768,
46
- alwaysOnTop: true,
47
- window: { decorations: 'hiddenTitleBar', shadow: true }
48
- })
49
- console.log('Window ID:', result.windowId)
50
- ```
51
-
52
- The `spawn()` method sends a dispatch to the native app and waits for acknowledgement. Window options include position, size, transparency, click-through, and title bar style.
53
-
54
- ## Navigation and JavaScript Evaluation
55
-
56
- Control window content after spawning.
57
-
58
- ```ts skip
59
- const handle = wm.window(result.windowId)
60
- await handle.navigate('https://news.ycombinator.com')
61
- console.log('Navigated')
62
-
63
- const title = await handle.eval('document.title')
64
- console.log('Page title:', title)
65
-
66
- await handle.focus()
67
- await handle.close()
68
- ```
69
-
70
- The `window()` method returns a `WindowHandle` for chainable operations on a specific window. Use `eval()` to run JavaScript in the window's web view.
71
-
72
- ## Screenshots and Video
73
-
74
- Capture visual output from windows.
75
-
76
- ```ts skip
77
- await wm.screengrab({
78
- windowId: result.windowId,
79
- path: './screenshot.png'
80
- })
81
- console.log('Screenshot saved')
82
-
83
- await wm.video({
84
- windowId: result.windowId,
85
- path: './recording.mp4',
86
- durationMs: 5000
87
- })
88
- console.log('Video recorded')
89
- ```
90
-
91
- Screenshots are saved as PNG. Video recording captures for the specified duration.
92
-
93
- ## Terminal Windows
94
-
95
- Spawn native terminal windows that render command output with ANSI support.
96
-
97
- ```ts skip
98
- const tty = await wm.spawnTTY({
99
- command: 'htop',
100
- title: 'System Monitor',
101
- width: 900,
102
- height: 600,
103
- cols: 120,
104
- rows: 40
105
- })
106
- console.log('TTY window:', tty.windowId)
107
- ```
108
-
109
- Terminal windows are read-only displays of process output. Closing the window terminates the process.
110
-
111
- ## IPC Communication
112
-
113
- Other features can send arbitrary messages over the socket connection.
114
-
115
- ```ts skip
116
- wm.listen()
117
- wm.on('message', (msg) => console.log('App says:', msg))
118
- wm.send({ id: 'abc', status: 'ready', speech: 'Window manager online' })
119
- ```
120
-
121
- The `message` event fires for any non-windowAck message from the native app.
122
-
123
- ## Summary
124
-
125
- The `windowManager` feature provides native window control through IPC with the LucaVoiceLauncher app. Spawn browser windows, navigate, evaluate JS, capture screenshots, and record video. Supports terminal windows for command output. Key methods: `spawn()`, `navigate()`, `eval()`, `screengrab()`, `video()`, `spawnTTY()`, `window()`.
@@ -1,249 +0,0 @@
1
- # Window Manager Fix
2
-
3
- ## Problem
4
-
5
- The current `windowManager` design allows any Luca process to call `listen()` on the same well-known Unix socket:
6
-
7
- - `~/Library/Application Support/LucaVoiceLauncher/ipc-window.sock`
8
-
9
- That means unrelated commands can compete for ownership of the app-facing socket. The current implementation makes this worse by doing the following on startup:
10
-
11
- 1. If the socket path exists, `unlinkSync(socketPath)`.
12
- 2. Bind a new server at the same path.
13
-
14
- This creates a race where one Luca process can steal the socket from another. The native `LucaVoiceLauncher` app then disconnects from the old server and reconnects to whichever process currently owns the path. If that process exits, the app falls into reconnect loops.
15
-
16
- This is the root cause of the observed behavior where:
17
-
18
- - the launcher sometimes connects successfully
19
- - the connection then drops unexpectedly
20
- - repeated `ipc connect failed` messages appear in the launcher log
21
-
22
- ## Design Goal
23
-
24
- We want:
25
-
26
- - one stable owner of the app-facing socket
27
- - many independent Luca commands able to trigger window actions
28
- - optional failover if the main owner dies
29
- - support for multiple launcher app clients over time, and optionally at once
30
-
31
- The key design rule is:
32
-
33
- > Many clients is fine. Many servers competing for the same well-known socket is not.
34
-
35
- ## Recommended Architecture
36
-
37
- ### 1. Single broker for the app socket
38
-
39
- Only one broker process may own:
40
-
41
- - `ipc-window.sock`
42
-
43
- The broker is responsible for:
44
-
45
- - accepting native launcher app connections
46
- - tracking connected app clients
47
- - routing window commands to the selected app client
48
- - receiving `windowAck`, `windowClosed`, and `terminalExited`
49
- - routing responses and lifecycle events back to the original requester
50
-
51
- ### 2. Separate control channel for Luca commands
52
-
53
- Luca commands should not bind the app-facing socket directly.
54
-
55
- Instead, they should talk to the broker over a separate channel, for example:
56
-
57
- - `~/Library/Application Support/LucaVoiceLauncher/ipc-window-control.sock`
58
-
59
- This control channel is for producers:
60
-
61
- - `luca main`
62
- - `luca workflow run ...`
63
- - `luca present`
64
- - scripts
65
- - background jobs
66
-
67
- These producers send requests to the broker, and the broker fans them out to the connected app client.
68
-
69
- ### 3. Broker supports multiple app clients
70
-
71
- The broker should replace the current single `_client` field with a registry:
72
-
73
- ```ts
74
- Map<string, ClientConnection>
75
- ```
76
-
77
- Each client should have:
78
-
79
- - `clientId`
80
- - `socket`
81
- - `buffer`
82
- - metadata if useful later, such as display, role, labels, or lastSeenAt
83
-
84
- This allows:
85
-
86
- - multiple launcher app instances
87
- - reconnect without confusing request ownership
88
- - future routing by target client
89
-
90
- ## Routing Model
91
-
92
- ### Producer -> broker
93
-
94
- Producer sends a request like:
95
-
96
- ```json
97
- {
98
- "type": "windowRequest",
99
- "requestId": "uuid",
100
- "originId": "uuid",
101
- "targetClientId": "optional",
102
- "window": {
103
- "action": "open",
104
- "url": "https://example.com"
105
- }
106
- }
107
- ```
108
-
109
- ### Broker -> app client
110
-
111
- Broker forwards the request to the chosen app client, preserving `requestId`.
112
-
113
- ### App client -> broker
114
-
115
- App replies with:
116
-
117
- - `windowAck`
118
- - `windowClosed`
119
- - `terminalExited`
120
-
121
- ### Broker -> producer
122
-
123
- Broker routes:
124
-
125
- - the `windowAck` back to the producer that originated the request
126
- - lifecycle events either to the originating producer, or to any subscribed producer
127
-
128
- ## Client Selection Policy
129
-
130
- The simplest policy is:
131
-
132
- - use the most recently connected healthy app client
133
-
134
- Later policies can support:
135
-
136
- - explicit `targetClientId`
137
- - labels like `role=presenter`
138
- - display-aware routing
139
- - sticky routing based on `windowId -> clientId`
140
-
141
- ## Leader Election / Failover
142
-
143
- If we want multiple `windowManager` instances to exist, they must not all behave as brokers.
144
-
145
- Instead:
146
-
147
- 1. Try connecting to the broker control socket.
148
- 2. If broker exists, act as a producer client.
149
- 3. If broker does not exist, try to acquire a broker lock.
150
- 4. If lock succeeds, become broker and bind both sockets.
151
- 5. If lock fails, retry broker connection and act as producer.
152
-
153
- Possible lock mechanisms:
154
-
155
- - lock file with `flock`
156
- - lock directory with atomic `mkdir`
157
- - local TCP/Unix registration endpoint
158
-
159
- The important constraint is:
160
-
161
- - only the elected broker binds `ipc-window.sock`
162
-
163
- All other instances must route through it.
164
-
165
- ## Why not let many processes bind the same socket?
166
-
167
- Because Unix domain socket paths are singular ownership points. A path is not a shared bus.
168
-
169
- If multiple processes all call `listen()` against the same path and delete stale files optimistically, they will:
170
-
171
- - steal the path from each other
172
- - disconnect the app unexpectedly
173
- - lose in-flight requests
174
- - create non-deterministic routing
175
-
176
- This is fundamentally the wrong abstraction.
177
-
178
- ## Backward-Compatible Migration
179
-
180
- We can migrate without breaking the public `windowManager.spawn()` API.
181
-
182
- ### Phase 1
183
-
184
- - Introduce a broker mode internally.
185
- - Add `ipc-window-control.sock`.
186
- - Keep the existing app protocol unchanged.
187
- - Make `windowManager.spawn()` talk to the broker when possible.
188
-
189
- ### Phase 2
190
-
191
- - Prevent non-broker processes from binding `ipc-window.sock`.
192
- - Replace blind `unlinkSync(socketPath)` with active listener detection.
193
- - Add broker election and failover.
194
-
195
- ### Phase 3
196
-
197
- - Add multi-client routing.
198
- - Add subscriptions for lifecycle events.
199
- - Add explicit target selection if needed.
200
-
201
- ## Minimal Fix if We Need Something Fast
202
-
203
- If we do not implement the full broker immediately, we should at least stop destroying active listeners.
204
-
205
- `listen()` should:
206
-
207
- 1. Attempt to connect to the existing socket.
208
- 2. If a listener is alive, do not unlink or rebind.
209
- 3. If the socket is dead, clean it up and bind.
210
-
211
- This does not solve multi-producer routing, but it prevents random Luca commands from stealing the app socket from a healthy broker.
212
-
213
- ## Proposed Internal Refactor
214
-
215
- Current state:
216
-
217
- - one process tries to be both broker and producer
218
- - one `_client`
219
- - one app-facing socket
220
-
221
- Target state:
222
-
223
- - broker owns app-facing socket
224
- - producers use control socket
225
- - broker stores:
226
- - `clients: Map<clientId, ClientConnection>`
227
- - `pendingRequests: Map<requestId, PendingRequest>`
228
- - `requestOrigins: Map<requestId, originConnection>`
229
- - `windowOwners: Map<windowId, clientId>`
230
-
231
- That separation gives us:
232
-
233
- - stable app connectivity
234
- - multi-command triggering
235
- - failover
236
- - room for multi-client routing
237
-
238
- ## Summary
239
-
240
- The right fix is not “allow many `listen()` calls on the same socket.”
241
-
242
- The right fix is:
243
-
244
- - one elected broker owns the app socket
245
- - many Luca processes talk to the broker
246
- - many app clients may connect to the broker
247
- - failover is implemented through broker election, not socket contention
248
-
249
- That preserves a stable connection for the launcher app while still allowing multiple people, commands, or workflows to trigger window operations.
@@ -1,86 +0,0 @@
1
- import { NodeContainer } from '../src/node/container'
2
-
3
- const args = new Set(process.argv.slice(2))
4
- const socketArg = process.argv.slice(2).find((arg) => arg.startsWith('--socket='))
5
- const socketPath = socketArg?.slice('--socket='.length)
6
- const shouldSpawnTTY = args.has('--spawn-tty')
7
- const shouldSpawnWindow = args.has('--spawn-window')
8
-
9
- const container = new NodeContainer({ cwd: process.cwd() })
10
- const wm = socketPath
11
- ? container.feature('windowManager', { socketPath })
12
- : container.feature('windowManager')
13
-
14
- wm.listen()
15
-
16
- const logCount = () => {
17
- console.log(`[state] windowCount=${wm.state.get('windowCount')}`)
18
- }
19
-
20
- wm.on('clientConnected', () => {
21
- console.log('[clientConnected] native launcher connected')
22
- })
23
-
24
- wm.on('clientDisconnected', () => {
25
- console.log('[clientDisconnected] native launcher disconnected')
26
- })
27
-
28
- wm.on('windowAck', (msg) => {
29
- console.log('[windowAck]', JSON.stringify(msg))
30
- logCount()
31
- })
32
-
33
- wm.on('windowClosed', (msg) => {
34
- console.log('[windowClosed]', JSON.stringify(msg))
35
- logCount()
36
- })
37
-
38
- wm.on('terminalExited', (msg) => {
39
- console.log('[terminalExited]', JSON.stringify(msg))
40
- })
41
-
42
- wm.on('message', (msg) => {
43
- if (msg?.type === 'windowAck' || msg?.type === 'windowClosed' || msg?.type === 'terminalExited') {
44
- return
45
- }
46
- console.log('[message]', JSON.stringify(msg))
47
- })
48
-
49
- let spawned = false
50
- wm.on('clientConnected', async () => {
51
- if (spawned) return
52
- spawned = true
53
-
54
- if (shouldSpawnWindow) {
55
- const opened = await wm.spawn({
56
- url: 'https://example.com',
57
- width: 900,
58
- height: 620,
59
- })
60
- console.log('[spawn-window]', opened)
61
- }
62
-
63
- if (shouldSpawnTTY) {
64
- const tty = await wm.spawnTTY({
65
- command: 'zsh',
66
- args: ['-lc', 'echo "window-manager lifecycle demo"; sleep 2; exit 7'],
67
- title: 'WM Lifecycle Demo',
68
- cols: 110,
69
- rows: 32,
70
- width: 960,
71
- height: 620,
72
- cwd: process.cwd(),
73
- })
74
- console.log('[spawn-tty]', tty)
75
- }
76
- })
77
-
78
- await container.sleep(150)
79
-
80
- console.log(`[ready] listening=${wm.isListening} socket=${wm.state.get('socketPath') ?? '<unset>'}`)
81
- if (!wm.isListening) {
82
- console.log(`[error] ${wm.state.get('lastError') ?? 'windowManager failed to bind socket'}`)
83
- console.log('[hint] try: bun run scripts/test-window-manager-lifecycle.ts --socket=/tmp/ipc-window.sock')
84
- }
85
- console.log('[usage] bun run scripts/test-window-manager-lifecycle.ts [--spawn-tty] [--spawn-window] [--socket=/path/to/ipc-window.sock]')
86
- console.log('[ready] waiting for native app connection...')
@@ -1,43 +0,0 @@
1
- import { NodeContainer } from '../src/node/container'
2
-
3
- const container = new NodeContainer({ cwd: process.cwd() })
4
- const wm = container.feature('windowManager')
5
-
6
- await wm.listen()
7
- console.log('Listening:', wm.isListening)
8
- console.log('Socket path:', wm.state.get('socketPath'))
9
-
10
- wm.on('clientConnected', () => {
11
- console.log('App connected')
12
- })
13
-
14
- wm.on('message', (msg) => {
15
- console.log('Message from app:', msg)
16
- })
17
-
18
- wm.on('clientDisconnected', () => {
19
- console.log('App disconnected')
20
- })
21
-
22
- // Wait for the app, then spawn a TTY running luca serve in the writing assistant playground
23
- wm.on('clientConnected', async () => {
24
- const result = await wm.spawnTTY({
25
- command: 'luca',
26
- args: ['serve', '--any-port'],
27
- cwd: '/Users/jon/@soederpop/playground/writing-assistant',
28
- title: 'Writing Assistant Server',
29
- cols: 120,
30
- rows: 40,
31
- width: 1000,
32
- height: 700,
33
- })
34
-
35
- console.log('Spawned TTY:', result)
36
-
37
- if (result.windowId) {
38
- console.log(`Window ID: ${result.windowId}`)
39
- console.log(`PID: ${result.pid}`)
40
- }
41
- })
42
-
43
- console.log('Waiting for native app to connect...')