stella-coder 5.3.4 → 5.3.5

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.
@@ -0,0 +1,254 @@
1
+ import { app, BrowserWindow, dialog, ipcMain } from "electron";
2
+ import { execFile, spawn, type ChildProcess } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+ import crypto from "node:crypto";
8
+ import { pathToFileURL } from "node:url";
9
+ import type {
10
+ AgentOutputEvent,
11
+ AgentStatusEvent,
12
+ DesktopSettings,
13
+ ProjectInfo,
14
+ SystemMetrics,
15
+ TaskRecord,
16
+ } from "./shared/contracts";
17
+
18
+ const execFileAsync = promisify(execFile);
19
+ const DEFAULT_SETTINGS: DesktopSettings = { model: "mimo-v2.5-free", recentWorkspaces: [] };
20
+ const MAX_HISTORY = 20;
21
+ const MAX_OUTPUT_LENGTH = 12000;
22
+ let mainWindow: BrowserWindow | null = null;
23
+ let activeProcess: ChildProcess | null = null;
24
+ let activeTask: TaskRecord | null = null;
25
+ let cpuSample: Array<{ idle: number; total: number }> | null = null;
26
+
27
+ function desktopDataPath(file: string) {
28
+ return path.join(app.getPath("userData"), file);
29
+ }
30
+
31
+ function sourceRoot() {
32
+ return app.isPackaged ? path.join(process.resourcesPath, "app.asar.unpacked") : path.resolve(__dirname, "../../..");
33
+ }
34
+
35
+ function cliPath() {
36
+ return path.join(sourceRoot(), "stella-cli", "index.mjs");
37
+ }
38
+
39
+ async function loadJson<T>(file: string, fallback: T): Promise<T> {
40
+ try {
41
+ return JSON.parse(await fs.readFile(file, "utf8")) as T;
42
+ } catch {
43
+ return fallback;
44
+ }
45
+ }
46
+
47
+ async function saveJson(file: string, value: unknown) {
48
+ await fs.mkdir(path.dirname(file), { recursive: true });
49
+ await fs.writeFile(file, JSON.stringify(value, null, 2), "utf8");
50
+ }
51
+
52
+ async function getSettings(): Promise<DesktopSettings> {
53
+ const saved = await loadJson<Partial<DesktopSettings>>(desktopDataPath("settings.json"), {});
54
+ return { ...DEFAULT_SETTINGS, ...saved, recentWorkspaces: saved.recentWorkspaces ?? [] };
55
+ }
56
+
57
+ async function saveSettings(next: Partial<DesktopSettings>) {
58
+ const settings = { ...(await getSettings()), ...next };
59
+ await saveJson(desktopDataPath("settings.json"), settings);
60
+ return settings;
61
+ }
62
+
63
+ async function getTasks() {
64
+ return loadJson<TaskRecord[]>(desktopDataPath("tasks.json"), []);
65
+ }
66
+
67
+ async function storeTask(task: TaskRecord) {
68
+ const tasks = await getTasks();
69
+ const withoutCurrent = tasks.filter((entry) => entry.id !== task.id);
70
+ await saveJson(desktopDataPath("tasks.json"), [task, ...withoutCurrent].slice(0, MAX_HISTORY));
71
+ }
72
+
73
+ function redactSecrets(value: string) {
74
+ return value
75
+ .replace(/Bearer\s+[^\s]+/gi, "Bearer [скрыто]")
76
+ .replace(/\b(?:sk-|zen-|opk_)[A-Za-z0-9_-]{8,}\b/g, "[скрыто]");
77
+ }
78
+
79
+ function emit(channel: "agent:output" | "agent:status" | "metrics:update", payload: AgentOutputEvent | AgentStatusEvent | SystemMetrics) {
80
+ mainWindow?.webContents.send(channel, payload);
81
+ }
82
+
83
+ function appendOutput(channel: "stdout" | "stderr", raw: string) {
84
+ if (!activeTask) return;
85
+ const text = redactSecrets(raw);
86
+ activeTask.output = (activeTask.output + text).slice(-MAX_OUTPUT_LENGTH);
87
+ emit("agent:output", { taskId: activeTask.id, channel, text });
88
+ }
89
+
90
+ async function getProjectInfo(workspace: string): Promise<ProjectInfo> {
91
+ const resolved = path.resolve(workspace);
92
+ const name = path.basename(resolved) || resolved;
93
+ try {
94
+ await fs.access(resolved);
95
+ } catch {
96
+ throw new Error("Папка проекта недоступна");
97
+ }
98
+ try {
99
+ const { stdout } = await execFileAsync("git", ["-C", resolved, "status", "--short", "--branch"], { windowsHide: true });
100
+ const lines = stdout.split(/\r?\n/).filter(Boolean);
101
+ const branchLine = lines.shift() ?? "";
102
+ const branch = branchLine.startsWith("## ") ? branchLine.slice(3).split("...")[0] : null;
103
+ const changedFiles = lines.map((line) => line.slice(3).trim()).filter(Boolean).slice(0, 100);
104
+ return { path: resolved, name, branch, changedFiles, isGitRepository: true };
105
+ } catch {
106
+ return { path: resolved, name, branch: null, changedFiles: [], isGitRepository: false };
107
+ }
108
+ }
109
+
110
+ function cpuTotals() {
111
+ return os.cpus().map((cpu) => ({
112
+ idle: cpu.times.idle,
113
+ total: Object.values(cpu.times).reduce((sum, value) => sum + value, 0),
114
+ }));
115
+ }
116
+
117
+ async function powerShellJson(command: string): Promise<unknown> {
118
+ const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-Command", command], { windowsHide: true, timeout: 5000 });
119
+ return JSON.parse(stdout.trim() || "null");
120
+ }
121
+
122
+ async function getMetrics(): Promise<SystemMetrics> {
123
+ const now = cpuTotals();
124
+ let cpuPercent: number | null = null;
125
+ if (cpuSample?.length === now.length) {
126
+ const idle = now.reduce((sum, entry, index) => sum + entry.idle - (cpuSample?.[index]?.idle ?? 0), 0);
127
+ const total = now.reduce((sum, entry, index) => sum + entry.total - (cpuSample?.[index]?.total ?? 0), 0);
128
+ cpuPercent = total > 0 ? Math.round((1 - idle / total) * 100) : null;
129
+ }
130
+ cpuSample = now;
131
+ const totalMemory = os.totalmem();
132
+ const memoryUsedGb = Number(((totalMemory - os.freemem()) / 1024 ** 3).toFixed(1));
133
+ const memoryTotalGb = Number((totalMemory / 1024 ** 3).toFixed(1));
134
+ let diskUsedGb: number | null = null;
135
+ let diskTotalGb: number | null = null;
136
+ let gpuName: string | null = null;
137
+ let gpuPercent: number | null = null;
138
+ let gpuTemperature: number | null = null;
139
+ try {
140
+ const disk = await powerShellJson("Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='C:'\" | Select-Object Size,FreeSpace | ConvertTo-Json -Compress") as { Size?: number; FreeSpace?: number };
141
+ if (disk?.Size) {
142
+ diskTotalGb = Number((disk.Size / 1024 ** 3).toFixed(1));
143
+ diskUsedGb = Number(((disk.Size - (disk.FreeSpace ?? 0)) / 1024 ** 3).toFixed(1));
144
+ }
145
+ } catch {}
146
+ try {
147
+ const { stdout } = await execFileAsync("nvidia-smi", ["--query-gpu=name,utilization.gpu,temperature.gpu", "--format=csv,noheader,nounits"], { windowsHide: true, timeout: 5000 });
148
+ const [name, usage, temperature] = stdout.trim().split(",").map((entry) => entry.trim());
149
+ gpuName = name || null;
150
+ gpuPercent = Number.isFinite(Number(usage)) ? Number(usage) : null;
151
+ gpuTemperature = Number.isFinite(Number(temperature)) ? Number(temperature) : null;
152
+ } catch {}
153
+ return { cpuPercent, memoryUsedGb, memoryTotalGb, diskUsedGb, diskTotalGb, gpuName, gpuPercent, gpuTemperature };
154
+ }
155
+
156
+ async function securityModule() {
157
+ const modulePath = path.join(sourceRoot(), "stella-cli", "security.mjs");
158
+ return import(pathToFileURL(modulePath).href) as Promise<{
159
+ getApiKey: () => { apiKey?: string } | null;
160
+ saveApiKey: (key: string) => { ok: boolean; error?: string };
161
+ deleteApiKey: () => { ok: boolean; error?: string };
162
+ }>;
163
+ }
164
+
165
+ async function finishActiveTask(status: TaskRecord["status"]) {
166
+ if (!activeTask) return;
167
+ activeTask.status = status;
168
+ activeTask.finishedAt = new Date().toISOString();
169
+ activeTask.durationMs = Date.now() - Date.parse(activeTask.startedAt);
170
+ const project = await getProjectInfo(activeTask.workspace);
171
+ activeTask.changedFiles = project.changedFiles;
172
+ await storeTask(activeTask);
173
+ emit("agent:status", { task: activeTask });
174
+ activeTask = null;
175
+ activeProcess = null;
176
+ }
177
+
178
+ async function startTask(input: { workspace: string; prompt: string; model: string }) {
179
+ if (activeProcess) throw new Error("Сейчас уже выполняется задача");
180
+ const prompt = input.prompt.trim();
181
+ if (!prompt) throw new Error("Введите задачу для Stella");
182
+ const project = await getProjectInfo(input.workspace);
183
+ const task: TaskRecord = {
184
+ id: crypto.randomUUID(), prompt, workspace: project.path, model: input.model,
185
+ status: "running", startedAt: new Date().toISOString(), output: "", changedFiles: [],
186
+ };
187
+ activeTask = task;
188
+ const child = spawn(process.execPath, [cliPath(), "-p", prompt, "--model", input.model], {
189
+ cwd: project.path,
190
+ windowsHide: true,
191
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: "1", FORCE_COLOR: "0" },
192
+ });
193
+ activeProcess = child;
194
+ emit("agent:status", { task });
195
+ child.stdout?.on("data", (chunk: Buffer) => appendOutput("stdout", chunk.toString()));
196
+ child.stderr?.on("data", (chunk: Buffer) => appendOutput("stderr", chunk.toString()));
197
+ child.on("error", (error) => appendOutput("stderr", `\n${error.message}\n`));
198
+ child.on("close", async (code) => {
199
+ const status = code === 0 ? "completed" : "failed";
200
+ await finishActiveTask(status);
201
+ });
202
+ return task;
203
+ }
204
+
205
+ async function stopTask(taskId: string) {
206
+ if (!activeProcess || !activeTask || activeTask.id !== taskId) return;
207
+ const pid = activeProcess.pid;
208
+ if (pid && process.platform === "win32") {
209
+ try { await execFileAsync("taskkill", ["/pid", String(pid), "/t", "/f"], { windowsHide: true }); } catch {}
210
+ } else {
211
+ activeProcess.kill();
212
+ }
213
+ await finishActiveTask("cancelled");
214
+ }
215
+
216
+ function createWindow() {
217
+ mainWindow = new BrowserWindow({
218
+ width: 1200, height: 760, minWidth: 980, minHeight: 660,
219
+ frame: false, backgroundColor: "#f8f8fc", show: false,
220
+ webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, sandbox: true },
221
+ });
222
+ const devUrl = process.env.ELECTRON_RENDERER_URL;
223
+ if (devUrl) void mainWindow.loadURL(devUrl); else void mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
224
+ mainWindow.once("ready-to-show", () => mainWindow?.show());
225
+ }
226
+
227
+ app.whenReady().then(() => {
228
+ ipcMain.handle("workspace:select", async () => {
229
+ const result = await dialog.showOpenDialog(mainWindow!, { properties: ["openDirectory", "createDirectory"] });
230
+ if (result.canceled || !result.filePaths[0]) return null;
231
+ const project = await getProjectInfo(result.filePaths[0]);
232
+ const settings = await getSettings();
233
+ await saveSettings({ recentWorkspaces: [project.path, ...settings.recentWorkspaces.filter((entry) => entry !== project.path)].slice(0, 8) });
234
+ return project;
235
+ });
236
+ ipcMain.handle("project:get", (_event, workspace: string) => getProjectInfo(workspace));
237
+ ipcMain.handle("settings:get", () => getSettings());
238
+ ipcMain.handle("settings:model", (_event, model: string) => saveSettings({ model }));
239
+ ipcMain.handle("key:has", async () => Boolean((await securityModule()).getApiKey()?.apiKey));
240
+ ipcMain.handle("key:save", async (_event, key: string) => (await securityModule()).saveApiKey(key));
241
+ ipcMain.handle("key:delete", async () => (await securityModule()).deleteApiKey());
242
+ ipcMain.handle("metrics:get", () => getMetrics());
243
+ ipcMain.handle("tasks:list", () => getTasks());
244
+ ipcMain.handle("task:start", (_event, input) => startTask(input));
245
+ ipcMain.handle("task:stop", (_event, id: string) => stopTask(id));
246
+ ipcMain.handle("window:minimize", () => mainWindow?.minimize());
247
+ ipcMain.handle("window:maximize", () => mainWindow?.isMaximized() ? mainWindow.unmaximize() : mainWindow?.maximize());
248
+ ipcMain.handle("window:close", () => mainWindow?.close());
249
+ setInterval(() => void getMetrics().then((metrics) => emit("metrics:update", metrics)), 2000);
250
+ createWindow();
251
+ app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
252
+ });
253
+
254
+ app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); });
@@ -0,0 +1,36 @@
1
+ import { contextBridge, ipcRenderer } from "electron";
2
+ import type { StellaDesktopApi } from "./shared/contracts";
3
+
4
+ const api: StellaDesktopApi = {
5
+ selectWorkspace: () => ipcRenderer.invoke("workspace:select"),
6
+ getProjectInfo: (workspace) => ipcRenderer.invoke("project:get", workspace),
7
+ getSettings: () => ipcRenderer.invoke("settings:get"),
8
+ setModel: (model) => ipcRenderer.invoke("settings:model", model),
9
+ hasApiKey: () => ipcRenderer.invoke("key:has"),
10
+ saveApiKey: (key) => ipcRenderer.invoke("key:save", key),
11
+ deleteApiKey: () => ipcRenderer.invoke("key:delete"),
12
+ getMetrics: () => ipcRenderer.invoke("metrics:get"),
13
+ getTasks: () => ipcRenderer.invoke("tasks:list"),
14
+ startTask: (input) => ipcRenderer.invoke("task:start", input),
15
+ stopTask: (taskId) => ipcRenderer.invoke("task:stop", taskId),
16
+ onMetrics: (listener) => {
17
+ const wrapped = (_event: Electron.IpcRendererEvent, payload: Parameters<typeof listener>[0]) => listener(payload);
18
+ ipcRenderer.on("metrics:update", wrapped);
19
+ return () => ipcRenderer.removeListener("metrics:update", wrapped);
20
+ },
21
+ onAgentOutput: (listener) => {
22
+ const wrapped = (_event: Electron.IpcRendererEvent, payload: Parameters<typeof listener>[0]) => listener(payload);
23
+ ipcRenderer.on("agent:output", wrapped);
24
+ return () => ipcRenderer.removeListener("agent:output", wrapped);
25
+ },
26
+ onAgentStatus: (listener) => {
27
+ const wrapped = (_event: Electron.IpcRendererEvent, payload: Parameters<typeof listener>[0]) => listener(payload);
28
+ ipcRenderer.on("agent:status", wrapped);
29
+ return () => ipcRenderer.removeListener("agent:status", wrapped);
30
+ },
31
+ minimizeWindow: () => ipcRenderer.invoke("window:minimize"),
32
+ toggleMaximizeWindow: () => ipcRenderer.invoke("window:maximize"),
33
+ closeWindow: () => ipcRenderer.invoke("window:close"),
34
+ };
35
+
36
+ contextBridge.exposeInMainWorld("stella", api);
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Stella Coder</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,88 @@
1
+ export type TaskStatus = "running" | "completed" | "failed" | "cancelled";
2
+
3
+ export interface ModelOption {
4
+ id: string;
5
+ label: string;
6
+ }
7
+
8
+ export const MODELS: ModelOption[] = [
9
+ { id: "mimo-v2.5-free", label: "MiMo V2.5 — бесплатно" },
10
+ { id: "deepseek-v4-flash-free", label: "DeepSeek V4 Flash — бесплатно" },
11
+ { id: "gpt-5.4", label: "GPT-5.4" },
12
+ { id: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
13
+ { id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
14
+ { id: "gpt-5-codex", label: "GPT-5 Codex" },
15
+ { id: "claude-sonnet-4.5", label: "Claude Sonnet 4.5" },
16
+ { id: "claude-opus-4.5", label: "Claude Opus 4.5" },
17
+ { id: "gemini-3-flash", label: "Gemini 3 Flash" },
18
+ { id: "deepseek-v4-pro", label: "DeepSeek V4 Pro" },
19
+ { id: "glm-5.2", label: "GLM 5.2" },
20
+ { id: "kimi-k2.6", label: "Kimi K2.6" },
21
+ ];
22
+
23
+ export interface DesktopSettings {
24
+ model: string;
25
+ recentWorkspaces: string[];
26
+ }
27
+
28
+ export interface ProjectInfo {
29
+ path: string;
30
+ name: string;
31
+ branch: string | null;
32
+ changedFiles: string[];
33
+ isGitRepository: boolean;
34
+ }
35
+
36
+ export interface SystemMetrics {
37
+ cpuPercent: number | null;
38
+ memoryUsedGb: number;
39
+ memoryTotalGb: number;
40
+ diskUsedGb: number | null;
41
+ diskTotalGb: number | null;
42
+ gpuName: string | null;
43
+ gpuPercent: number | null;
44
+ gpuTemperature: number | null;
45
+ }
46
+
47
+ export interface TaskRecord {
48
+ id: string;
49
+ prompt: string;
50
+ workspace: string;
51
+ model: string;
52
+ status: TaskStatus;
53
+ startedAt: string;
54
+ finishedAt?: string;
55
+ durationMs?: number;
56
+ output: string;
57
+ changedFiles: string[];
58
+ }
59
+
60
+ export interface AgentOutputEvent {
61
+ taskId: string;
62
+ channel: "stdout" | "stderr";
63
+ text: string;
64
+ }
65
+
66
+ export interface AgentStatusEvent {
67
+ task: TaskRecord;
68
+ }
69
+
70
+ export interface StellaDesktopApi {
71
+ selectWorkspace(): Promise<ProjectInfo | null>;
72
+ getProjectInfo(workspace: string): Promise<ProjectInfo>;
73
+ getSettings(): Promise<DesktopSettings>;
74
+ setModel(model: string): Promise<DesktopSettings>;
75
+ hasApiKey(): Promise<boolean>;
76
+ saveApiKey(key: string): Promise<{ ok: boolean; error?: string }>;
77
+ deleteApiKey(): Promise<{ ok: boolean; error?: string }>;
78
+ getMetrics(): Promise<SystemMetrics>;
79
+ getTasks(): Promise<TaskRecord[]>;
80
+ startTask(input: { workspace: string; prompt: string; model: string }): Promise<TaskRecord>;
81
+ stopTask(taskId: string): Promise<void>;
82
+ onMetrics(listener: (metrics: SystemMetrics) => void): () => void;
83
+ onAgentOutput(listener: (event: AgentOutputEvent) => void): () => void;
84
+ onAgentStatus(listener: (event: AgentStatusEvent) => void): () => void;
85
+ minimizeWindow(): Promise<void>;
86
+ toggleMaximizeWindow(): Promise<void>;
87
+ closeWindow(): Promise<void>;
88
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "Node",
6
+ "rootDir": ".",
7
+ "outDir": "dist/main",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "types": ["node", "electron"]
12
+ },
13
+ "include": ["main.ts", "preload.ts", "shared/**/*.ts"]
14
+ }
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import path from "node:path";
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ root: path.resolve(__dirname, "renderer"),
8
+ base: "./",
9
+ build: {
10
+ outDir: path.resolve(__dirname, "dist/renderer"),
11
+ emptyOutDir: false,
12
+ },
13
+ server: {
14
+ port: 5173,
15
+ strictPort: true,
16
+ },
17
+ });
package/install.ps1 ADDED
@@ -0,0 +1,39 @@
1
+ $nd = "$env:USERPROFILE\StellaNode"
2
+ $np = "$nd\node-v22.14.0-win-x64"
3
+
4
+ Write-Host " [1/3] Downloading Node.js..."
5
+ if (Test-Path "$np\node.exe") {
6
+ Write-Host " [OK] Already have Node.js"
7
+ } else {
8
+ Invoke-WebRequest -Uri "https://nodejs.org/dist/v22.14.0/node-v22.14.0-win-x64.zip" -OutFile "$env:TEMP\node.zip" -UseBasicParsing
9
+ if (-not (Test-Path "$env:TEMP\node.zip")) {
10
+ Write-Host " [FAIL] Download failed"
11
+ exit 1
12
+ }
13
+ if (-not (Test-Path $nd)) {
14
+ New-Item -ItemType Directory -Path $nd -Force | Out-Null
15
+ }
16
+ Expand-Archive -Path "$env:TEMP\node.zip" -DestinationPath $nd -Force
17
+ Remove-Item "$env:TEMP\node.zip" -Force
18
+ if (-not (Test-Path "$np\node.exe")) {
19
+ Write-Host " [FAIL] Extract failed"
20
+ exit 1
21
+ }
22
+ Write-Host " [OK] Node.js installed"
23
+ }
24
+
25
+ Write-Host " [2/3] Installing stella-coder..."
26
+ $npm = "$np\node_modules\npm\bin\npm-cli.js"
27
+ & "$np\node.exe" "$npm" install -g stella-coder@latest
28
+ if ($LASTEXITCODE -ne 0) {
29
+ Write-Host " [FAIL] npm install failed"
30
+ exit 1
31
+ }
32
+ Write-Host " [OK] stella-coder installed"
33
+
34
+ Write-Host " [3/3] Fixing stella wrapper..."
35
+ Remove-Item "$np\stella.ps1" -Force -ErrorAction SilentlyContinue
36
+
37
+ Write-Host ""
38
+ Write-Host " DONE!"
39
+ Write-Host " Run: & `"$np\stella.cmd`""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stella-coder",
3
- "version": "5.3.4",
3
+ "version": "5.3.5",
4
4
  "private": false,
5
5
  "description": "Stella Coder 5.0 — AI coding agent with Telegram bot, huge context, TDD, Git ecosystem, presentations, computer control, smart home, Office automation, and antivirus",
6
6
  "main": "stella-cli/index.mjs",
@@ -12,6 +12,9 @@
12
12
  "stellar-av": "node antimalware/index.mjs",
13
13
  "dev": "next dev",
14
14
  "build": "next build",
15
+ "desktop:dev": "concurrently -k -n renderer,main,electron \"vite --config desktop/vite.config.ts\" \"tsc --watch -p desktop/tsconfig.main.json\" \"wait-on tcp:5173 file:desktop/dist/main/main.js && cross-env ELECTRON_RENDERER_URL=http://localhost:5173 electron desktop/dist/main/main.js\"",
16
+ "desktop:build": "tsc -p desktop/tsconfig.main.json && vite build --config desktop/vite.config.ts",
17
+ "desktop:dist": "pnpm desktop:build && electron-builder --win nsis",
15
18
  "build:cli": "node stella-cli/build.mjs",
16
19
  "start": "next start",
17
20
  "lint": "eslint ."
@@ -48,16 +51,54 @@
48
51
  "zod": "^4.4.3"
49
52
  },
50
53
  "devDependencies": {
54
+ "@vitejs/plugin-react": "^6.0.3",
51
55
  "@eslint/eslintrc": "^3.3.0",
52
56
  "@tailwindcss/postcss": "^4.2.0",
53
57
  "@types/node": "^24",
54
58
  "@types/react": "^19",
55
59
  "@types/react-dom": "^19",
60
+ "concurrently": "^10.0.3",
61
+ "cross-env": "^10.1.0",
62
+ "electron": "^43.1.0",
63
+ "electron-builder": "^26.15.3",
56
64
  "eslint": "^9.28.0",
57
65
  "eslint-config-next": "^16.2.0",
58
66
  "postcss": "^8.5",
59
67
  "tailwindcss": "^4.2.0",
60
- "typescript": "5.7.3"
68
+ "typescript": "5.7.3",
69
+ "vite": "^8.1.4",
70
+ "wait-on": "^9.0.10"
71
+ },
72
+ "build": {
73
+ "appId": "com.stella.coder",
74
+ "productName": "Stella Coder",
75
+ "directories": {
76
+ "output": "release-desktop"
77
+ },
78
+ "extraMetadata": {
79
+ "main": "desktop/dist/main/main.js"
80
+ },
81
+ "files": [
82
+ "desktop/dist/**/*",
83
+ "stella-cli/**/*",
84
+ "antimalware/**/*",
85
+ "package.json"
86
+ ],
87
+ "asar": true,
88
+ "asarUnpack": [
89
+ "stella-cli/**/*",
90
+ "antimalware/**/*"
91
+ ],
92
+ "win": {
93
+ "target": "nsis",
94
+ "artifactName": "Stella-Coder-Setup-${version}.${ext}"
95
+ },
96
+ "nsis": {
97
+ "oneClick": false,
98
+ "allowToChangeInstallationDirectory": true,
99
+ "createDesktopShortcut": true,
100
+ "createStartMenuShortcut": true
101
+ }
61
102
  },
62
103
  "pnpm": {
63
104
  "overrides": {
@@ -52,7 +52,7 @@ import {
52
52
  generateAdminCode, listAuthorizedUsers,
53
53
  } from "./telegram-bot.mjs"
54
54
 
55
- const VERSION = "5.3.4"
55
+ const VERSION = "5.3.5"
56
56
  const CONFIG_DIR = path.join(os.homedir(), ".stella")
57
57
  const CONFIG_PATH = path.join(CONFIG_DIR, "config.json")
58
58
  const HISTORY_PATH = path.join(CONFIG_DIR, "history.json")
@@ -369,6 +369,41 @@ async function runTurn(userText) {
369
369
  }
370
370
  console.log("\n")
371
371
  } else {
372
+ // Try streaming first, fallback to generateText if no text in 10s
373
+ let gotText = false
374
+ const fallbackTimer = setTimeout(async () => {
375
+ if (!gotText) {
376
+ stopSpinner()
377
+ console.log(dim(" ⏳ Streaming stuck, using generateText fallback..."))
378
+ try {
379
+ const res = await generateText({
380
+ model: getModel(state.model),
381
+ system: systemPrompt(),
382
+ messages: state.messages,
383
+ tools,
384
+ maxSteps: 30,
385
+ abortSignal: controller.signal,
386
+ })
387
+ if (res.text) {
388
+ process.stdout.write("\n" + violet("⏺ ") + res.text + "\n")
389
+ state.messages.push({ role: "assistant", content: res.text })
390
+ for (const toolMsg of res.response.messages) {
391
+ if (toolMsg.role === "assistant") state.messages.push(toolMsg)
392
+ }
393
+ const usage = res.usage
394
+ const inTok = usage.promptTokens ?? 0
395
+ const outTok = usage.completionTokens ?? 0
396
+ state.totalTokens.input += inTok
397
+ state.totalTokens.output += outTok
398
+ const dur = ((Date.now() - t0) / 1000).toFixed(1)
399
+ console.log(dim(" ") + darkGray(`⏱ ${dur}s · ↑${inTok} ↓${outTok} ток · ${blue(state.model)}`))
400
+ }
401
+ } catch (e2) {
402
+ console.log("\n" + red("✗ Ошибка fallback: ") + String(e2?.message || e2).slice(0, 300))
403
+ }
404
+ }
405
+ }, 10000)
406
+
372
407
  result = streamText({
373
408
  model: getModel(state.model),
374
409
  system: systemPrompt(),
@@ -391,6 +426,8 @@ async function runTurn(userText) {
391
426
  switch (part.type) {
392
427
  case "text-delta": {
393
428
  stopSpinner()
429
+ gotText = true
430
+ clearTimeout(fallbackTimer)
394
431
  if (firstText) {
395
432
  process.stdout.write("\n" + violet("⏺ ") )
396
433
  firstText = false
@@ -430,6 +467,7 @@ async function runTurn(userText) {
430
467
  }
431
468
  case "finish": {
432
469
  stopSpinner()
470
+ clearTimeout(fallbackTimer)
433
471
  renderer.flush()
434
472
  break
435
473
  }
@@ -0,0 +1,23 @@
1
+ import { streamText } from "ai";
2
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
3
+
4
+ const z = createOpenAICompatible({
5
+ name: "z",
6
+ baseURL: "https://opencode.ai/zen/v1",
7
+ apiKey: "sk-U6RlsOyn7qmvEsYEQ0c4syqtCpff0GQXd2z8Wr9m5Px2tpxrtU7GiUvTRM4arfw8",
8
+ });
9
+
10
+ const m = z.chatModel("mimo-v2.5-free");
11
+ console.log("Starting...");
12
+
13
+ const r = streamText({
14
+ model: m,
15
+ messages: [{ role: "user", content: "say hi" }],
16
+ });
17
+
18
+ console.log("Waiting for stream...");
19
+
20
+ for await (const p of r.fullStream) {
21
+ if (p.type === "text-delta") process.stdout.write(p.text);
22
+ }
23
+ console.log("\nDONE");