snow-flow 10.0.101 → 10.0.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/cmd/tui/app.tsx +2 -16
- package/src/server/routes/tui-client.ts +1 -131
- package/src/server/server.ts +12 -15
package/package.json
CHANGED
package/src/cli/cmd/tui/app.tsx
CHANGED
|
@@ -117,20 +117,15 @@ export function tui(input: {
|
|
|
117
117
|
_onUpgrade = input.onUpgrade
|
|
118
118
|
// promise to prevent immediate exit
|
|
119
119
|
return new Promise<void>(async (resolve) => {
|
|
120
|
-
console.log("[snow-flow] detecting terminal background...")
|
|
121
120
|
const mode = await getTerminalBackgroundColor()
|
|
122
|
-
console.log("[snow-flow] background:", mode)
|
|
123
121
|
const onExit = async () => {
|
|
124
122
|
await input.onExit?.()
|
|
125
123
|
resolve()
|
|
126
124
|
}
|
|
127
125
|
|
|
128
|
-
const isRemote = !!process.env.OPENCODE_REMOTE_TUI
|
|
129
|
-
console.log("[snow-flow] starting render...", isRemote ? "(remote mode)" : "(local mode)")
|
|
130
126
|
try {
|
|
131
127
|
await render(
|
|
132
128
|
() => {
|
|
133
|
-
console.log("[snow-flow] component tree evaluating...")
|
|
134
129
|
return (
|
|
135
130
|
<ErrorBoundary
|
|
136
131
|
fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} mode={mode} />}
|
|
@@ -180,7 +175,6 @@ export function tui(input: {
|
|
|
180
175
|
targetFps: 60,
|
|
181
176
|
gatherStats: false,
|
|
182
177
|
exitOnCtrlC: false,
|
|
183
|
-
...(isRemote ? { remote: true } : {}),
|
|
184
178
|
useKittyKeyboard: process.env.OPENCODE_DISABLE_KITTY_KEYBOARD ? undefined : {},
|
|
185
179
|
consoleOptions: {
|
|
186
180
|
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
|
|
@@ -192,19 +186,15 @@ export function tui(input: {
|
|
|
192
186
|
},
|
|
193
187
|
},
|
|
194
188
|
)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
console.log(`[snow-flow] render() FAILED: ${e instanceof Error ? e.message : e}`)
|
|
198
|
-
if (e instanceof Error && e.stack) console.log(e.stack)
|
|
189
|
+
} catch {
|
|
190
|
+
// render failure handled by ErrorBoundary
|
|
199
191
|
}
|
|
200
192
|
})
|
|
201
193
|
}
|
|
202
194
|
|
|
203
195
|
function App() {
|
|
204
|
-
console.log("[snow-flow] App component mounting...")
|
|
205
196
|
const route = useRoute()
|
|
206
197
|
const dimensions = useTerminalDimensions()
|
|
207
|
-
console.log("[snow-flow] dimensions:", dimensions().width, "x", dimensions().height)
|
|
208
198
|
const renderer = useRenderer()
|
|
209
199
|
renderer.disableStdoutInterception()
|
|
210
200
|
const dialog = useDialog()
|
|
@@ -229,10 +219,6 @@ function App() {
|
|
|
229
219
|
}
|
|
230
220
|
const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))
|
|
231
221
|
|
|
232
|
-
createEffect(() => {
|
|
233
|
-
console.log(JSON.stringify(route.data))
|
|
234
|
-
})
|
|
235
|
-
|
|
236
222
|
// Update terminal window title based on current route and session
|
|
237
223
|
createEffect(() => {
|
|
238
224
|
if (!terminalTitleEnabled() || Flag.OPENCODE_DISABLE_TERMINAL_TITLE) return
|
|
@@ -1,138 +1,8 @@
|
|
|
1
1
|
import { Hono } from "hono"
|
|
2
2
|
import { lazy } from "../../util/lazy"
|
|
3
3
|
|
|
4
|
-
const HTML = `<!doctype html>
|
|
5
|
-
<html lang="en">
|
|
6
|
-
<head>
|
|
7
|
-
<meta charset="UTF-8">
|
|
8
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
9
|
-
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
10
|
-
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
11
|
-
<meta name="mobile-web-app-capable" content="yes">
|
|
12
|
-
<meta name="theme-color" content="#0a0a1a">
|
|
13
|
-
<title>Snow-Flow TUI</title>
|
|
14
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3ClinearGradient id='m' x1='16' y1='9' x2='16' y2='23' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0%25' stop-color='%23FFFFFF'/%3E%3Cstop offset='100%25' stop-color='%2300D9FF'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M4 23 L10 9 L16 15 L22 9 L28 23 Z' fill='url(%23m)' stroke='%2300D9FF' stroke-width='1.2' stroke-linejoin='round'/%3E%3C/svg%3E">
|
|
15
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
|
16
|
-
<style>
|
|
17
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
18
|
-
html, body { height: 100%; overflow: hidden; background: #0a0a1a; color: #e2e8f0; font-family: -apple-system, system-ui, sans-serif; }
|
|
19
|
-
body { display: flex; flex-direction: column; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }
|
|
20
|
-
#status { height: 32px; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; background: #111127; border-bottom: 1px solid #1e1e3a; font-size: 12px; flex-shrink: 0; }
|
|
21
|
-
#status .left { display: flex; align-items: center; gap: 8px; }
|
|
22
|
-
#status .dot { width: 8px; height: 8px; border-radius: 50%; background: #444; }
|
|
23
|
-
#status .dot.connected { background: #22c55e; }
|
|
24
|
-
#status .dot.connecting { background: #eab308; animation: pulse 1s infinite; }
|
|
25
|
-
#status .dot.error { background: #ef4444; }
|
|
26
|
-
#status .label { color: #94a3b8; }
|
|
27
|
-
#terminal-container { flex: 1; min-height: 0; }
|
|
28
|
-
#terminal-container .xterm { height: 100%; padding: 4px; }
|
|
29
|
-
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
30
|
-
@media (max-width: 600px) {
|
|
31
|
-
#status { height: 28px; font-size: 11px; padding: 0 8px; }
|
|
32
|
-
}
|
|
33
|
-
</style>
|
|
34
|
-
</head>
|
|
35
|
-
<body>
|
|
36
|
-
<div id="status">
|
|
37
|
-
<div class="left">
|
|
38
|
-
<div id="dot" class="dot connecting"></div>
|
|
39
|
-
<span id="status-text" class="label">Connecting...</span>
|
|
40
|
-
</div>
|
|
41
|
-
<span class="label">Snow-Flow TUI</span>
|
|
42
|
-
</div>
|
|
43
|
-
<div id="terminal-container"></div>
|
|
44
|
-
<script type="module">
|
|
45
|
-
import { Terminal } from "https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/+esm"
|
|
46
|
-
import { FitAddon } from "https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/+esm"
|
|
47
|
-
import { WebLinksAddon } from "https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/+esm"
|
|
48
|
-
|
|
49
|
-
const dot = document.getElementById("dot")
|
|
50
|
-
const statusText = document.getElementById("status-text")
|
|
51
|
-
|
|
52
|
-
function setStatus(state, text) {
|
|
53
|
-
dot.className = "dot " + state
|
|
54
|
-
statusText.textContent = text
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
|
|
58
|
-
|
|
59
|
-
const term = new Terminal({
|
|
60
|
-
fontSize: isMobile ? 13 : 15,
|
|
61
|
-
fontFamily: "Menlo, 'Courier New', monospace",
|
|
62
|
-
theme: {
|
|
63
|
-
background: "#0a0a1a",
|
|
64
|
-
foreground: "#e2e8f0",
|
|
65
|
-
cursor: "#4f8ff7",
|
|
66
|
-
cursorAccent: "#0a0a1a",
|
|
67
|
-
selectionBackground: "rgba(79, 143, 247, 0.3)",
|
|
68
|
-
black: "#1a1a2e",
|
|
69
|
-
red: "#ef4444",
|
|
70
|
-
green: "#22c55e",
|
|
71
|
-
yellow: "#eab308",
|
|
72
|
-
blue: "#3b82f6",
|
|
73
|
-
magenta: "#a855f7",
|
|
74
|
-
cyan: "#06b6d4",
|
|
75
|
-
white: "#e2e8f0",
|
|
76
|
-
},
|
|
77
|
-
cursorBlink: true,
|
|
78
|
-
convertEol: true,
|
|
79
|
-
allowProposedApi: true,
|
|
80
|
-
scrollback: 5000,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
const fit = new FitAddon()
|
|
84
|
-
term.loadAddon(fit)
|
|
85
|
-
term.loadAddon(new WebLinksAddon())
|
|
86
|
-
term.open(document.getElementById("terminal-container"))
|
|
87
|
-
fit.fit()
|
|
88
|
-
|
|
89
|
-
let ws
|
|
90
|
-
let reconnectTimer
|
|
91
|
-
|
|
92
|
-
function connect() {
|
|
93
|
-
setStatus("connecting", "Connecting...")
|
|
94
|
-
const proto = location.protocol === "https:" ? "wss:" : "ws:"
|
|
95
|
-
ws = new WebSocket(proto + "//" + location.host + "/tui-ws/ws?cols=" + term.cols + "&rows=" + term.rows)
|
|
96
|
-
|
|
97
|
-
ws.onopen = () => setStatus("connected", "Connected")
|
|
98
|
-
|
|
99
|
-
ws.onmessage = (e) => term.write(typeof e.data === "string" ? e.data : new Uint8Array(e.data))
|
|
100
|
-
|
|
101
|
-
ws.onclose = (e) => {
|
|
102
|
-
setStatus("error", "Disconnected — reconnecting...")
|
|
103
|
-
clearTimeout(reconnectTimer)
|
|
104
|
-
reconnectTimer = setTimeout(connect, 2000)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
ws.onerror = () => {
|
|
108
|
-
setStatus("error", "Connection error")
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
term.onData((data) => {
|
|
113
|
-
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data)
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
function doFit() {
|
|
117
|
-
fit.fit()
|
|
118
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
119
|
-
ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }))
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
window.addEventListener("resize", doFit)
|
|
124
|
-
if (window.visualViewport) {
|
|
125
|
-
window.visualViewport.addEventListener("resize", doFit)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
connect()
|
|
129
|
-
term.focus()
|
|
130
|
-
</script>
|
|
131
|
-
</body>
|
|
132
|
-
</html>`
|
|
133
|
-
|
|
134
4
|
export const TuiClientRoutes = lazy(() =>
|
|
135
5
|
new Hono().get("/", (c) => {
|
|
136
|
-
return c.
|
|
6
|
+
return c.redirect("/")
|
|
137
7
|
}),
|
|
138
8
|
)
|
package/src/server/server.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler
|
|
|
5
5
|
import { Hono } from "hono"
|
|
6
6
|
import { cors } from "hono/cors"
|
|
7
7
|
import { streamSSE } from "hono/streaming"
|
|
8
|
-
import {
|
|
8
|
+
import { serveStatic } from "hono/bun"
|
|
9
|
+
import path from "path"
|
|
9
10
|
import { basicAuth } from "hono/basic-auth"
|
|
10
11
|
import z from "zod"
|
|
11
12
|
import { Provider } from "../provider/provider"
|
|
@@ -531,21 +532,17 @@ export namespace Server {
|
|
|
531
532
|
})
|
|
532
533
|
},
|
|
533
534
|
)
|
|
534
|
-
.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
535
|
+
.use("/assets/*", serveStatic({ root: "./packages/app/dist" }))
|
|
536
|
+
.get("/*", async (c) => {
|
|
537
|
+
const filePath = path.join(process.cwd(), "packages/app/dist", c.req.path)
|
|
538
|
+
const file = Bun.file(filePath)
|
|
539
|
+
if (await file.exists()) {
|
|
540
|
+
return new Response(file)
|
|
541
|
+
}
|
|
542
|
+
// SPA fallback — serve index.html for all non-asset routes
|
|
543
|
+
return new Response(Bun.file(path.join(process.cwd(), "packages/app/dist/index.html")), {
|
|
544
|
+
headers: { "Content-Type": "text/html" },
|
|
543
545
|
})
|
|
544
|
-
response.headers.set(
|
|
545
|
-
"Content-Security-Policy",
|
|
546
|
-
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' data:",
|
|
547
|
-
)
|
|
548
|
-
return response
|
|
549
546
|
}) as unknown as Hono,
|
|
550
547
|
)
|
|
551
548
|
|