snow-flow 10.0.114 → 10.0.116
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 +5 -7
- package/src/provider/provider.ts +135 -24
- package/src/pty/index.ts +25 -4
- package/src/server/routes/tui-ws.ts +18 -14
package/package.json
CHANGED
package/src/cli/cmd/tui/app.tsx
CHANGED
|
@@ -115,12 +115,12 @@ export function tui(input: {
|
|
|
115
115
|
onUpgrade?: () => Promise<{ success: boolean; version?: string; error?: string }>
|
|
116
116
|
}) {
|
|
117
117
|
_onUpgrade = input.onUpgrade
|
|
118
|
-
const
|
|
118
|
+
const skipThemeDetection = !!process.env.OPENCODE_SKIP_THEME_DETECTION || !!process.env.OPENCODE_REMOTE_TUI
|
|
119
119
|
// promise to prevent immediate exit
|
|
120
120
|
return new Promise<void>(async (resolve) => {
|
|
121
121
|
let mode: "dark" | "light" = "dark"
|
|
122
|
-
if (
|
|
123
|
-
console.log("[snow-flow]
|
|
122
|
+
if (skipThemeDetection) {
|
|
123
|
+
console.log("[snow-flow] skipping theme detection")
|
|
124
124
|
} else {
|
|
125
125
|
console.log("[snow-flow] detecting theme...")
|
|
126
126
|
mode = await getTerminalBackgroundColor()
|
|
@@ -184,7 +184,7 @@ export function tui(input: {
|
|
|
184
184
|
targetFps: 60,
|
|
185
185
|
gatherStats: false,
|
|
186
186
|
exitOnCtrlC: false,
|
|
187
|
-
|
|
187
|
+
// No remote: true — let opentui render normally to the PTY
|
|
188
188
|
useKittyKeyboard: process.env.OPENCODE_DISABLE_KITTY_KEYBOARD ? undefined : {},
|
|
189
189
|
consoleOptions: {
|
|
190
190
|
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
|
|
@@ -675,9 +675,7 @@ function App() {
|
|
|
675
675
|
kv.set("telemetry_enabled", !enabled)
|
|
676
676
|
await Config.updateGlobal({ telemetry: !enabled }).catch(() => {})
|
|
677
677
|
toast.show({
|
|
678
|
-
message: enabled
|
|
679
|
-
? "Telemetry disabled. Takes effect next session."
|
|
680
|
-
: "Telemetry enabled. Thank you!",
|
|
678
|
+
message: enabled ? "Telemetry disabled. Takes effect next session." : "Telemetry enabled. Thank you!",
|
|
681
679
|
variant: "info",
|
|
682
680
|
})
|
|
683
681
|
dialog.clear()
|
package/src/provider/provider.ts
CHANGED
|
@@ -86,15 +86,23 @@ export namespace Provider {
|
|
|
86
86
|
options?: Record<string, any>
|
|
87
87
|
}>
|
|
88
88
|
|
|
89
|
-
function createSSEStream(
|
|
89
|
+
function createSSEStream(
|
|
90
|
+
content: string,
|
|
91
|
+
model: string,
|
|
92
|
+
usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number },
|
|
93
|
+
): string[] {
|
|
90
94
|
const id = `chatcmpl-${crypto.randomUUID()}`
|
|
91
95
|
const chunks: string[] = []
|
|
92
96
|
|
|
93
97
|
// Role chunk
|
|
94
|
-
chunks.push(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
chunks.push(
|
|
99
|
+
`data: ${JSON.stringify({
|
|
100
|
+
id,
|
|
101
|
+
object: "chat.completion.chunk",
|
|
102
|
+
model,
|
|
103
|
+
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }],
|
|
104
|
+
})}\n\n`,
|
|
105
|
+
)
|
|
98
106
|
|
|
99
107
|
// Content chunks (~10 chars per chunk for smooth streaming
|
|
100
108
|
const words = content.split(/(\s+)/)
|
|
@@ -102,26 +110,38 @@ export namespace Provider {
|
|
|
102
110
|
for (const word of words) {
|
|
103
111
|
current += word
|
|
104
112
|
if (current.length >= 10 || word.includes("\n")) {
|
|
105
|
-
chunks.push(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
chunks.push(
|
|
114
|
+
`data: ${JSON.stringify({
|
|
115
|
+
id,
|
|
116
|
+
object: "chat.completion.chunk",
|
|
117
|
+
model,
|
|
118
|
+
choices: [{ index: 0, delta: { content: current }, finish_reason: null }],
|
|
119
|
+
})}\n\n`,
|
|
120
|
+
)
|
|
109
121
|
current = ""
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
124
|
if (current) {
|
|
113
|
-
chunks.push(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
chunks.push(
|
|
126
|
+
`data: ${JSON.stringify({
|
|
127
|
+
id,
|
|
128
|
+
object: "chat.completion.chunk",
|
|
129
|
+
model,
|
|
130
|
+
choices: [{ index: 0, delta: { content: current }, finish_reason: null }],
|
|
131
|
+
})}\n\n`,
|
|
132
|
+
)
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
// Finish chunk
|
|
120
|
-
chunks.push(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
chunks.push(
|
|
137
|
+
`data: ${JSON.stringify({
|
|
138
|
+
id,
|
|
139
|
+
object: "chat.completion.chunk",
|
|
140
|
+
model,
|
|
141
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
142
|
+
usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
143
|
+
})}\n\n`,
|
|
144
|
+
)
|
|
125
145
|
|
|
126
146
|
chunks.push("data: [DONE]\n\n")
|
|
127
147
|
return chunks
|
|
@@ -538,6 +558,82 @@ export namespace Provider {
|
|
|
538
558
|
},
|
|
539
559
|
}
|
|
540
560
|
},
|
|
561
|
+
ollama: async (input) => {
|
|
562
|
+
const host = (Env.get("OLLAMA_HOST") || "http://localhost:11434").replace(/\/+$/, "")
|
|
563
|
+
try {
|
|
564
|
+
const response = await fetch(`${host}/api/tags`, {
|
|
565
|
+
signal: AbortSignal.timeout(2000),
|
|
566
|
+
})
|
|
567
|
+
if (!response.ok) return { autoload: false }
|
|
568
|
+
const data = (await response.json()) as { models?: Array<{ name: string; details?: { parameter_size?: string; family?: string } }> }
|
|
569
|
+
if (!data.models?.length) return { autoload: false }
|
|
570
|
+
|
|
571
|
+
for (const model of data.models) {
|
|
572
|
+
if (input.models[model.name]) continue
|
|
573
|
+
input.models[model.name] = fromModelsDevModel(
|
|
574
|
+
{ id: "ollama", name: "Ollama", npm: "@ai-sdk/openai-compatible", api: `${host}/v1`, env: ["OLLAMA_HOST"], models: {} },
|
|
575
|
+
{
|
|
576
|
+
id: model.name,
|
|
577
|
+
name: model.name,
|
|
578
|
+
family: model.details?.family ?? model.name.split(":")[0],
|
|
579
|
+
attachment: false,
|
|
580
|
+
reasoning: false,
|
|
581
|
+
tool_call: true,
|
|
582
|
+
temperature: true,
|
|
583
|
+
release_date: "2024-01-01",
|
|
584
|
+
modalities: { input: ["text"], output: ["text"] },
|
|
585
|
+
cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
|
|
586
|
+
limit: { context: 131072, output: 32768 },
|
|
587
|
+
options: {},
|
|
588
|
+
},
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
autoload: true,
|
|
593
|
+
options: { baseURL: `${host}/v1`, apiKey: "ollama" },
|
|
594
|
+
}
|
|
595
|
+
} catch {
|
|
596
|
+
return { autoload: false }
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
lmstudio: async (input) => {
|
|
600
|
+
const host = (Env.get("LMSTUDIO_HOST") || "http://127.0.0.1:1234").replace(/\/+$/, "")
|
|
601
|
+
try {
|
|
602
|
+
const response = await fetch(`${host}/v1/models`, {
|
|
603
|
+
signal: AbortSignal.timeout(2000),
|
|
604
|
+
})
|
|
605
|
+
if (!response.ok) return { autoload: false }
|
|
606
|
+
const data = (await response.json()) as { data?: Array<{ id: string }> }
|
|
607
|
+
if (!data.data?.length) return { autoload: false }
|
|
608
|
+
|
|
609
|
+
for (const model of data.data) {
|
|
610
|
+
if (input.models[model.id]) continue
|
|
611
|
+
input.models[model.id] = fromModelsDevModel(
|
|
612
|
+
{ id: "lmstudio", name: "LM Studio", npm: "@ai-sdk/openai-compatible", api: `${host}/v1`, env: ["LMSTUDIO_HOST"], models: {} },
|
|
613
|
+
{
|
|
614
|
+
id: model.id,
|
|
615
|
+
name: model.id,
|
|
616
|
+
family: model.id.split("/").pop()?.split("-")[0] ?? model.id,
|
|
617
|
+
attachment: false,
|
|
618
|
+
reasoning: false,
|
|
619
|
+
tool_call: true,
|
|
620
|
+
temperature: true,
|
|
621
|
+
release_date: "2024-01-01",
|
|
622
|
+
modalities: { input: ["text"], output: ["text"] },
|
|
623
|
+
cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
|
|
624
|
+
limit: { context: 131072, output: 32768 },
|
|
625
|
+
options: {},
|
|
626
|
+
},
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
autoload: true,
|
|
631
|
+
options: { baseURL: `${host}/v1`, apiKey: "lm-studio" },
|
|
632
|
+
}
|
|
633
|
+
} catch {
|
|
634
|
+
return { autoload: false }
|
|
635
|
+
}
|
|
636
|
+
},
|
|
541
637
|
cerebras: async () => {
|
|
542
638
|
return {
|
|
543
639
|
autoload: false,
|
|
@@ -565,7 +661,12 @@ export namespace Provider {
|
|
|
565
661
|
})
|
|
566
662
|
if (response.ok) {
|
|
567
663
|
const data = await response.json()
|
|
568
|
-
if (
|
|
664
|
+
if (
|
|
665
|
+
data.success &&
|
|
666
|
+
data.instance?.instanceUrl &&
|
|
667
|
+
data.instance?.clientId &&
|
|
668
|
+
data.instance?.clientSecret
|
|
669
|
+
) {
|
|
569
670
|
await Auth.set("servicenow", {
|
|
570
671
|
type: "servicenow-oauth",
|
|
571
672
|
instance: data.instance.instanceUrl,
|
|
@@ -777,6 +878,16 @@ export namespace Provider {
|
|
|
777
878
|
const modelsDev = await ModelsDev.get()
|
|
778
879
|
const database = mapValues(modelsDev, fromModelsDevProvider)
|
|
779
880
|
|
|
881
|
+
// Ensure local providers exist in the database so CUSTOM_LOADERS can discover them
|
|
882
|
+
for (const local of [
|
|
883
|
+
{ id: "ollama", name: "Ollama", npm: "@ai-sdk/openai-compatible", api: "http://localhost:11434/v1", env: ["OLLAMA_HOST"] },
|
|
884
|
+
{ id: "lmstudio", name: "LM Studio", npm: "@ai-sdk/openai-compatible", api: "http://127.0.0.1:1234/v1", env: ["LMSTUDIO_HOST"] },
|
|
885
|
+
] as const) {
|
|
886
|
+
if (!database[local.id]) {
|
|
887
|
+
database[local.id] = fromModelsDevProvider({ id: local.id, name: local.name, npm: local.npm, api: local.api, env: [...local.env], models: {} })
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
780
891
|
const disabled = new Set(config.disabled_providers ?? [])
|
|
781
892
|
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
|
|
782
893
|
|
|
@@ -1201,10 +1312,10 @@ export namespace Provider {
|
|
|
1201
1312
|
// Error response
|
|
1202
1313
|
else if (unwrapped && unwrapped.success === false && unwrapped.error) {
|
|
1203
1314
|
log.warn("servicenow-llm: ServiceNow error", { error: unwrapped.error })
|
|
1204
|
-
return new Response(
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
)
|
|
1315
|
+
return new Response(JSON.stringify({ error: { message: unwrapped.error, type: "servicenow_error" } }), {
|
|
1316
|
+
status: 500,
|
|
1317
|
+
headers: { "content-type": "application/json" },
|
|
1318
|
+
})
|
|
1208
1319
|
}
|
|
1209
1320
|
}
|
|
1210
1321
|
// Direct OpenAI format (no wrapper)
|
|
@@ -1234,7 +1345,7 @@ export namespace Provider {
|
|
|
1234
1345
|
headers: {
|
|
1235
1346
|
"content-type": "text/event-stream",
|
|
1236
1347
|
"cache-control": "no-cache",
|
|
1237
|
-
|
|
1348
|
+
connection: "keep-alive",
|
|
1238
1349
|
},
|
|
1239
1350
|
})
|
|
1240
1351
|
} else {
|
package/src/pty/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export namespace Pty {
|
|
|
40
40
|
cwd: z.string().optional(),
|
|
41
41
|
title: z.string().optional(),
|
|
42
42
|
env: z.record(z.string(), z.string()).optional(),
|
|
43
|
+
mode: z.enum(["shell", "tui"]).optional(),
|
|
43
44
|
})
|
|
44
45
|
|
|
45
46
|
export type CreateInput = z.infer<typeof CreateInput>
|
|
@@ -95,15 +96,35 @@ export namespace Pty {
|
|
|
95
96
|
|
|
96
97
|
export async function create(input: CreateInput) {
|
|
97
98
|
const id = Identifier.create("pty", false)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
let command = input.command || Shell.preferred()
|
|
100
|
+
let args = input.args || []
|
|
101
|
+
let cwd = input.cwd || Instance.directory
|
|
102
|
+
let extraEnv: Record<string, string> = {}
|
|
103
|
+
|
|
104
|
+
if (input.mode === "tui") {
|
|
105
|
+
const packageRoot = new URL("../../", import.meta.url).pathname.replace(/\/$/, "")
|
|
106
|
+
command = "bun"
|
|
107
|
+
args = [
|
|
108
|
+
"run",
|
|
109
|
+
"--conditions=browser",
|
|
110
|
+
"src/index.ts",
|
|
111
|
+
"--connect",
|
|
112
|
+
`http://127.0.0.1:${process.env.PORT || "4096"}`,
|
|
113
|
+
]
|
|
114
|
+
cwd = packageRoot
|
|
115
|
+
extraEnv = {
|
|
116
|
+
OPENCODE_SKIP_THEME_DETECTION: "1",
|
|
117
|
+
OPENCODE_DISABLE_KITTY_KEYBOARD: "1",
|
|
118
|
+
COLORTERM: "truecolor",
|
|
119
|
+
FORCE_COLOR: "3",
|
|
120
|
+
}
|
|
121
|
+
} else if (command.endsWith("sh")) {
|
|
101
122
|
args.push("-l")
|
|
102
123
|
}
|
|
103
124
|
|
|
104
|
-
const cwd = input.cwd || Instance.directory
|
|
105
125
|
const env = {
|
|
106
126
|
...process.env,
|
|
127
|
+
...extraEnv,
|
|
107
128
|
...input.env,
|
|
108
129
|
TERM: "xterm-256color",
|
|
109
130
|
SNOW_CODE_TERMINAL: "1",
|
|
@@ -40,21 +40,25 @@ async function spawnTui(cols: number, rows: number, env?: Record<string, string>
|
|
|
40
40
|
const { spawn } = await import("bun-pty")
|
|
41
41
|
const id = crypto.randomUUID()
|
|
42
42
|
|
|
43
|
-
const ptyProcess = spawn(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
const ptyProcess = spawn(
|
|
44
|
+
"bun",
|
|
45
|
+
["run", "--conditions=browser", "src/index.ts", "--connect", "http://localhost:4096"],
|
|
46
|
+
{
|
|
47
|
+
name: "xterm-256color",
|
|
48
|
+
cols,
|
|
49
|
+
rows,
|
|
50
|
+
cwd: new URL("../../../", import.meta.url).pathname.replace(/\/$/, ""),
|
|
51
|
+
env: {
|
|
52
|
+
...process.env,
|
|
53
|
+
...env,
|
|
54
|
+
TERM: "xterm-256color",
|
|
55
|
+
COLORTERM: "truecolor",
|
|
56
|
+
FORCE_COLOR: "3",
|
|
57
|
+
OPENCODE_SKIP_THEME_DETECTION: "1",
|
|
58
|
+
OPENCODE_DISABLE_KITTY_KEYBOARD: "1",
|
|
59
|
+
},
|
|
56
60
|
},
|
|
57
|
-
|
|
61
|
+
)
|
|
58
62
|
|
|
59
63
|
const session: TuiSession = { pty: ptyProcess, lastActivity: Date.now() }
|
|
60
64
|
sessions.set(id, session)
|