weifuwu 0.18.8 → 0.18.10
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/dist/index.js +144 -136
- package/dist/live.d.ts +1 -0
- package/dist/tailwind.d.ts +2 -1
- package/opencode/ui/app.css +1 -0
- package/opencode/ui/layout.tsx +14 -0
- package/opencode/ui/page.tsx +294 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -5130,7 +5130,7 @@ function ensureCertificates(config) {
|
|
|
5130
5130
|
import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
|
|
5131
5131
|
|
|
5132
5132
|
// opencode/rest.ts
|
|
5133
|
-
import { join as
|
|
5133
|
+
import { join as join5 } from "node:path";
|
|
5134
5134
|
|
|
5135
5135
|
// ssr.ts
|
|
5136
5136
|
import { createElement } from "react";
|
|
@@ -5489,9 +5489,112 @@ function layout(path2) {
|
|
|
5489
5489
|
};
|
|
5490
5490
|
}
|
|
5491
5491
|
|
|
5492
|
+
// tailwind.ts
|
|
5493
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
5494
|
+
import { join as join3, relative, resolve as resolve5 } from "node:path";
|
|
5495
|
+
|
|
5496
|
+
// live.ts
|
|
5497
|
+
import chokidar from "chokidar";
|
|
5498
|
+
var clients = /* @__PURE__ */ new Set();
|
|
5499
|
+
function broadcastReload() {
|
|
5500
|
+
for (const ws of clients) {
|
|
5501
|
+
try {
|
|
5502
|
+
ws.send("reload");
|
|
5503
|
+
} catch {
|
|
5504
|
+
clients.delete(ws);
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
function liveReload(opts) {
|
|
5509
|
+
const r = new Router();
|
|
5510
|
+
r.ws("/__weifuwu/livereload", {
|
|
5511
|
+
open(ws) {
|
|
5512
|
+
clients.add(ws);
|
|
5513
|
+
ws.on("close", () => clients.delete(ws));
|
|
5514
|
+
ws.on("error", () => clients.delete(ws));
|
|
5515
|
+
}
|
|
5516
|
+
});
|
|
5517
|
+
const watcher = chokidar.watch(opts.dirs, {
|
|
5518
|
+
ignored: /(^|[/\\])\.|node_modules|[/\\]\.weifuwu[/\\]/,
|
|
5519
|
+
ignoreInitial: true
|
|
5520
|
+
});
|
|
5521
|
+
watcher.on("change", (path2) => {
|
|
5522
|
+
if (!/\.tsx?$/.test(path2)) return;
|
|
5523
|
+
clearCompileCache();
|
|
5524
|
+
setTimeout(broadcastReload, 50);
|
|
5525
|
+
});
|
|
5526
|
+
r.close = () => {
|
|
5527
|
+
watcher.close();
|
|
5528
|
+
clients.clear();
|
|
5529
|
+
};
|
|
5530
|
+
return r;
|
|
5531
|
+
}
|
|
5532
|
+
|
|
5533
|
+
// tailwind.ts
|
|
5534
|
+
var isDev2 = process.env.NODE_ENV !== "production";
|
|
5535
|
+
var extraSources = /* @__PURE__ */ new Set();
|
|
5536
|
+
function addTailwindSource(dir) {
|
|
5537
|
+
extraSources.add(resolve5(dir));
|
|
5538
|
+
}
|
|
5539
|
+
function tailwind(dir) {
|
|
5540
|
+
const cssDir = resolve5(dir);
|
|
5541
|
+
const cssPath = join3(cssDir, "app.css");
|
|
5542
|
+
let compiledCss = "";
|
|
5543
|
+
let twWatcher = null;
|
|
5544
|
+
return async (req, ctx, next) => {
|
|
5545
|
+
const url = new URL(req.url);
|
|
5546
|
+
if (!compiledCss) compiledCss = await compile(cssPath, cssDir);
|
|
5547
|
+
const stylePath = (ctx.mountPath || "") + "/__wfw/style.css";
|
|
5548
|
+
if (url.pathname === stylePath) {
|
|
5549
|
+
return new Response(compiledCss || "", {
|
|
5550
|
+
headers: { "content-type": "text/css; charset=utf-8" }
|
|
5551
|
+
});
|
|
5552
|
+
}
|
|
5553
|
+
ctx.compiledTailwindCss = compiledCss;
|
|
5554
|
+
if (isDev2 && !twWatcher) {
|
|
5555
|
+
twWatcher = watchFile(cssPath, () => {
|
|
5556
|
+
compiledCss = "";
|
|
5557
|
+
broadcastReload();
|
|
5558
|
+
});
|
|
5559
|
+
}
|
|
5560
|
+
return next(req, ctx);
|
|
5561
|
+
};
|
|
5562
|
+
}
|
|
5563
|
+
async function compile(cssPath, cssDir) {
|
|
5564
|
+
try {
|
|
5565
|
+
if (!existsSync3(cssPath)) {
|
|
5566
|
+
mkdirSync2(cssDir, { recursive: true });
|
|
5567
|
+
writeFileSync(cssPath, '@import "tailwindcss"\n', "utf-8");
|
|
5568
|
+
}
|
|
5569
|
+
const { default: tailwindPlugin } = await import("@tailwindcss/postcss");
|
|
5570
|
+
const { default: postcss } = await import("postcss");
|
|
5571
|
+
let src = readFileSync3(cssPath, "utf-8");
|
|
5572
|
+
src = `@source "./";
|
|
5573
|
+
${src}`;
|
|
5574
|
+
for (const srcDir of extraSources) {
|
|
5575
|
+
const rel = relative(cssDir, srcDir) || ".";
|
|
5576
|
+
src = `@source "${rel.startsWith(".") ? rel : "./" + rel}";
|
|
5577
|
+
${src}`;
|
|
5578
|
+
}
|
|
5579
|
+
const result = await postcss([tailwindPlugin()]).process(src, { from: cssPath });
|
|
5580
|
+
return result.css;
|
|
5581
|
+
} catch (err) {
|
|
5582
|
+
console.warn("Tailwind CSS processing failed:", err.message);
|
|
5583
|
+
return "";
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
function watchFile(path2, onChange) {
|
|
5587
|
+
let watcher = null;
|
|
5588
|
+
import("chokidar").then((chokidar2) => {
|
|
5589
|
+
watcher = chokidar2.default.watch(resolve5(path2), { persistent: false });
|
|
5590
|
+
watcher.on("change", onChange);
|
|
5591
|
+
});
|
|
5592
|
+
return watcher;
|
|
5593
|
+
}
|
|
5594
|
+
|
|
5492
5595
|
// opencode/session.ts
|
|
5493
5596
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5494
|
-
import { join as
|
|
5597
|
+
import { join as join4 } from "node:path";
|
|
5495
5598
|
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
5496
5599
|
var sessions = pgTable("_opencode_sessions", {
|
|
5497
5600
|
id: uuid("id"),
|
|
@@ -5528,7 +5631,7 @@ async function createSession(sql2, opts, cwd, mountPath) {
|
|
|
5528
5631
|
}
|
|
5529
5632
|
function computeSessionWorkspace(cwd, mountPath, sessionId) {
|
|
5530
5633
|
const name = !mountPath || mountPath === "/" ? "default" : mountPath.replace(/^\//, "");
|
|
5531
|
-
return
|
|
5634
|
+
return join4(cwd, ".sessions", name, sessionId);
|
|
5532
5635
|
}
|
|
5533
5636
|
async function getSession(sql2, id3) {
|
|
5534
5637
|
const { data: rows } = await sessions.readMany(sql2, { id: id3, active: true });
|
|
@@ -5744,8 +5847,8 @@ function createBashTool(ctx) {
|
|
|
5744
5847
|
// opencode/tools/read.ts
|
|
5745
5848
|
import { tool as tool4 } from "ai";
|
|
5746
5849
|
import { z as z6 } from "zod";
|
|
5747
|
-
import { readFileSync as
|
|
5748
|
-
import { resolve as
|
|
5850
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
5851
|
+
import { resolve as resolve6 } from "node:path";
|
|
5749
5852
|
function createReadTool(ctx) {
|
|
5750
5853
|
return tool4({
|
|
5751
5854
|
description: "Read file contents. Supports offset and limit for reading specific line ranges.",
|
|
@@ -5755,11 +5858,11 @@ function createReadTool(ctx) {
|
|
|
5755
5858
|
limit: z6.number().optional().describe("Number of lines to read")
|
|
5756
5859
|
}),
|
|
5757
5860
|
execute: async ({ path: path2, offset, limit }) => {
|
|
5758
|
-
const resolved =
|
|
5861
|
+
const resolved = resolve6(ctx.workspace, path2);
|
|
5759
5862
|
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
5760
5863
|
return { error: "Path not allowed", content: null, totalLines: 0 };
|
|
5761
5864
|
}
|
|
5762
|
-
const content =
|
|
5865
|
+
const content = readFileSync4(resolved, "utf-8");
|
|
5763
5866
|
const lines = content.split("\n");
|
|
5764
5867
|
const totalLines = lines.length;
|
|
5765
5868
|
if (offset !== void 0) {
|
|
@@ -5786,8 +5889,8 @@ function createReadTool(ctx) {
|
|
|
5786
5889
|
// opencode/tools/write.ts
|
|
5787
5890
|
import { tool as tool5 } from "ai";
|
|
5788
5891
|
import { z as z7 } from "zod";
|
|
5789
|
-
import { writeFileSync, mkdirSync as
|
|
5790
|
-
import { resolve as
|
|
5892
|
+
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "node:fs";
|
|
5893
|
+
import { resolve as resolve7, dirname as dirname3 } from "node:path";
|
|
5791
5894
|
function createWriteTool(ctx) {
|
|
5792
5895
|
return tool5({
|
|
5793
5896
|
description: "Create or overwrite a file. Parent directories are created automatically.",
|
|
@@ -5796,12 +5899,12 @@ function createWriteTool(ctx) {
|
|
|
5796
5899
|
content: z7.string().describe("File content")
|
|
5797
5900
|
}),
|
|
5798
5901
|
execute: async ({ path: path2, content }) => {
|
|
5799
|
-
const resolved =
|
|
5902
|
+
const resolved = resolve7(ctx.workspace, path2);
|
|
5800
5903
|
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
5801
5904
|
return { error: "Path not allowed" };
|
|
5802
5905
|
}
|
|
5803
|
-
|
|
5804
|
-
|
|
5906
|
+
mkdirSync3(dirname3(resolved), { recursive: true });
|
|
5907
|
+
writeFileSync2(resolved, content, "utf-8");
|
|
5805
5908
|
return { path: path2, size: content.length };
|
|
5806
5909
|
}
|
|
5807
5910
|
});
|
|
@@ -5810,8 +5913,8 @@ function createWriteTool(ctx) {
|
|
|
5810
5913
|
// opencode/tools/edit.ts
|
|
5811
5914
|
import { tool as tool6 } from "ai";
|
|
5812
5915
|
import { z as z8 } from "zod";
|
|
5813
|
-
import { readFileSync as
|
|
5814
|
-
import { resolve as
|
|
5916
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5917
|
+
import { resolve as resolve8 } from "node:path";
|
|
5815
5918
|
function createEditTool(ctx) {
|
|
5816
5919
|
return tool6({
|
|
5817
5920
|
description: "Perform exact string replacements in a file. If oldString appears multiple times, provide more surrounding context.",
|
|
@@ -5822,18 +5925,18 @@ function createEditTool(ctx) {
|
|
|
5822
5925
|
replaceAll: z8.boolean().default(false).describe("Replace all occurrences")
|
|
5823
5926
|
}),
|
|
5824
5927
|
execute: async ({ path: path2, oldString, newString, replaceAll }) => {
|
|
5825
|
-
const resolved =
|
|
5928
|
+
const resolved = resolve8(ctx.workspace, path2);
|
|
5826
5929
|
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
5827
5930
|
return { error: "Path not allowed" };
|
|
5828
5931
|
}
|
|
5829
|
-
const content =
|
|
5932
|
+
const content = readFileSync5(resolved, "utf-8");
|
|
5830
5933
|
if (replaceAll) {
|
|
5831
5934
|
if (!content.includes(oldString)) {
|
|
5832
5935
|
return { error: "oldString not found in file", replaced: 0 };
|
|
5833
5936
|
}
|
|
5834
5937
|
const count = content.split(oldString).length - 1;
|
|
5835
5938
|
const result2 = content.replaceAll(oldString, newString);
|
|
5836
|
-
|
|
5939
|
+
writeFileSync3(resolved, result2, "utf-8");
|
|
5837
5940
|
return { path: path2, replaced: count };
|
|
5838
5941
|
}
|
|
5839
5942
|
const firstIdx = content.indexOf(oldString);
|
|
@@ -5845,7 +5948,7 @@ function createEditTool(ctx) {
|
|
|
5845
5948
|
return { error: "Found multiple matches. Provide more surrounding context in oldString.", replaced: 0 };
|
|
5846
5949
|
}
|
|
5847
5950
|
const result = content.replace(oldString, newString);
|
|
5848
|
-
|
|
5951
|
+
writeFileSync3(resolved, result, "utf-8");
|
|
5849
5952
|
return { path: path2, replaced: 1 };
|
|
5850
5953
|
}
|
|
5851
5954
|
});
|
|
@@ -5855,8 +5958,8 @@ function createEditTool(ctx) {
|
|
|
5855
5958
|
import { tool as tool7 } from "ai";
|
|
5856
5959
|
import { z as z9 } from "zod";
|
|
5857
5960
|
import { execFileSync } from "node:child_process";
|
|
5858
|
-
import { resolve as
|
|
5859
|
-
import { existsSync as
|
|
5961
|
+
import { resolve as resolve9 } from "node:path";
|
|
5962
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
5860
5963
|
function createGrepTool(ctx) {
|
|
5861
5964
|
return tool7({
|
|
5862
5965
|
description: "Search file contents using regex. Supports file type filtering and context lines.",
|
|
@@ -5867,10 +5970,10 @@ function createGrepTool(ctx) {
|
|
|
5867
5970
|
context: z9.number().default(0).describe("Number of context lines before and after each match")
|
|
5868
5971
|
}),
|
|
5869
5972
|
execute: async ({ pattern, include, path: path2, context }) => {
|
|
5870
|
-
const searchDir = path2 ?
|
|
5973
|
+
const searchDir = path2 ? resolve9(ctx.workspace, path2) : ctx.workspace;
|
|
5871
5974
|
try {
|
|
5872
5975
|
let stdout;
|
|
5873
|
-
if (
|
|
5976
|
+
if (existsSync4("/usr/bin/rg") || existsSync4("/usr/local/bin/rg")) {
|
|
5874
5977
|
const args = ["-n"];
|
|
5875
5978
|
if (context > 0) args.push("-C", String(context));
|
|
5876
5979
|
if (include) args.push("-g", include);
|
|
@@ -5899,7 +6002,7 @@ function createGrepTool(ctx) {
|
|
|
5899
6002
|
import { tool as tool8 } from "ai";
|
|
5900
6003
|
import { z as z10 } from "zod";
|
|
5901
6004
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5902
|
-
import { resolve as
|
|
6005
|
+
import { resolve as resolve10 } from "node:path";
|
|
5903
6006
|
function createGlobTool(ctx) {
|
|
5904
6007
|
return tool8({
|
|
5905
6008
|
description: "Find files matching a glob pattern.",
|
|
@@ -5908,7 +6011,7 @@ function createGlobTool(ctx) {
|
|
|
5908
6011
|
path: z10.string().optional().describe("Subdirectory relative to workspace")
|
|
5909
6012
|
}),
|
|
5910
6013
|
execute: async ({ pattern, path: path2 }) => {
|
|
5911
|
-
const searchDir = path2 ?
|
|
6014
|
+
const searchDir = path2 ? resolve10(ctx.workspace, path2) : ctx.workspace;
|
|
5912
6015
|
try {
|
|
5913
6016
|
const stdout = execFileSync2("find", [
|
|
5914
6017
|
searchDir,
|
|
@@ -6124,8 +6227,9 @@ async function buildRouter4(deps) {
|
|
|
6124
6227
|
});
|
|
6125
6228
|
try {
|
|
6126
6229
|
const uiDir = new URL("../opencode/ui/", import.meta.url).pathname;
|
|
6127
|
-
|
|
6128
|
-
router.
|
|
6230
|
+
addTailwindSource(uiDir);
|
|
6231
|
+
router.use(layout(join5(uiDir, "layout.tsx")));
|
|
6232
|
+
router.get("/", ssr(join5(uiDir, "page.tsx")));
|
|
6129
6233
|
} catch (e) {
|
|
6130
6234
|
console.warn("[opencode] UI not available:", e);
|
|
6131
6235
|
}
|
|
@@ -6133,17 +6237,17 @@ async function buildRouter4(deps) {
|
|
|
6133
6237
|
}
|
|
6134
6238
|
|
|
6135
6239
|
// opencode/ws.ts
|
|
6136
|
-
var
|
|
6240
|
+
var clients2 = /* @__PURE__ */ new WeakMap();
|
|
6137
6241
|
function createWSHandler2(deps) {
|
|
6138
6242
|
const { sql: sql2, model, workspace, systemPrompt, skills, skillsRegistry, permissions, pendingQuestions } = deps;
|
|
6139
6243
|
return {
|
|
6140
6244
|
open(ws, ctx) {
|
|
6141
6245
|
const userId = ctx.user?.id ?? 0;
|
|
6142
6246
|
const mountPath = ctx.mountPath ?? "";
|
|
6143
|
-
|
|
6247
|
+
clients2.set(ws, { userId, mountPath });
|
|
6144
6248
|
},
|
|
6145
6249
|
async message(ws, ctx, data) {
|
|
6146
|
-
const client =
|
|
6250
|
+
const client = clients2.get(ws);
|
|
6147
6251
|
if (!client) return;
|
|
6148
6252
|
let msg;
|
|
6149
6253
|
try {
|
|
@@ -6240,17 +6344,17 @@ function createWSHandler2(deps) {
|
|
|
6240
6344
|
}
|
|
6241
6345
|
},
|
|
6242
6346
|
close(ws) {
|
|
6243
|
-
const client =
|
|
6347
|
+
const client = clients2.get(ws);
|
|
6244
6348
|
if (client) {
|
|
6245
6349
|
client.abortController?.abort();
|
|
6246
|
-
|
|
6350
|
+
clients2.delete(ws);
|
|
6247
6351
|
}
|
|
6248
6352
|
},
|
|
6249
6353
|
error(ws, _ctx, _err) {
|
|
6250
|
-
const client =
|
|
6354
|
+
const client = clients2.get(ws);
|
|
6251
6355
|
if (client) {
|
|
6252
6356
|
client.abortController?.abort();
|
|
6253
|
-
|
|
6357
|
+
clients2.delete(ws);
|
|
6254
6358
|
}
|
|
6255
6359
|
}
|
|
6256
6360
|
};
|
|
@@ -6260,7 +6364,7 @@ function createWSHandler2(deps) {
|
|
|
6260
6364
|
import { readFile } from "node:fs/promises";
|
|
6261
6365
|
import { glob } from "node:fs/promises";
|
|
6262
6366
|
import { homedir } from "node:os";
|
|
6263
|
-
import { resolve as
|
|
6367
|
+
import { resolve as resolve11 } from "node:path";
|
|
6264
6368
|
import { parse as parseYaml } from "yaml";
|
|
6265
6369
|
var SEARCH_DIRS = [
|
|
6266
6370
|
(ws) => `${ws}/.opencode/skills`,
|
|
@@ -6298,7 +6402,7 @@ async function scanDir(dir) {
|
|
|
6298
6402
|
try {
|
|
6299
6403
|
const files = [];
|
|
6300
6404
|
for await (const entry of glob("*/SKILL.md", { cwd: dir })) {
|
|
6301
|
-
const skill = await parseSkillFile(
|
|
6405
|
+
const skill = await parseSkillFile(resolve11(dir, entry));
|
|
6302
6406
|
if (skill) files.push(skill);
|
|
6303
6407
|
}
|
|
6304
6408
|
return files;
|
|
@@ -6633,8 +6737,8 @@ function analytics(options) {
|
|
|
6633
6737
|
|
|
6634
6738
|
// preferences.ts
|
|
6635
6739
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
6636
|
-
import { existsSync as
|
|
6637
|
-
import { join as
|
|
6740
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
6741
|
+
import { join as join6, resolve as resolve12 } from "node:path";
|
|
6638
6742
|
var defaults = {
|
|
6639
6743
|
locale: { default: "en", cookie: "locale", fromAcceptLanguage: true },
|
|
6640
6744
|
theme: { default: "system", cookie: "theme" }
|
|
@@ -6683,7 +6787,7 @@ async function handlePrefSwitch(req, value, cookieName, load) {
|
|
|
6683
6787
|
});
|
|
6684
6788
|
}
|
|
6685
6789
|
function preferences(options) {
|
|
6686
|
-
const dir = options.dir ?
|
|
6790
|
+
const dir = options.dir ? resolve12(options.dir) : void 0;
|
|
6687
6791
|
const localeOpts = { ...defaults.locale, ...options.locale };
|
|
6688
6792
|
const themeOpts = { ...defaults.theme, ...options.theme };
|
|
6689
6793
|
const cache2 = /* @__PURE__ */ new Map();
|
|
@@ -6695,8 +6799,8 @@ function preferences(options) {
|
|
|
6695
6799
|
if (!validLocale(locale)) return {};
|
|
6696
6800
|
const cached = cache2.get(locale);
|
|
6697
6801
|
if (cached) return cached;
|
|
6698
|
-
const filePath =
|
|
6699
|
-
if (!
|
|
6802
|
+
const filePath = join6(dir, `${locale}.json`);
|
|
6803
|
+
if (!existsSync5(filePath)) return {};
|
|
6700
6804
|
try {
|
|
6701
6805
|
const content = await readFile2(filePath, "utf-8");
|
|
6702
6806
|
const data = JSON.parse(content);
|
|
@@ -8116,102 +8220,6 @@ function registerWorker(url) {
|
|
|
8116
8220
|
};
|
|
8117
8221
|
}
|
|
8118
8222
|
|
|
8119
|
-
// live.ts
|
|
8120
|
-
import chokidar from "chokidar";
|
|
8121
|
-
var clients2 = /* @__PURE__ */ new Set();
|
|
8122
|
-
function broadcastReload() {
|
|
8123
|
-
for (const ws of clients2) {
|
|
8124
|
-
try {
|
|
8125
|
-
ws.send("reload");
|
|
8126
|
-
} catch {
|
|
8127
|
-
clients2.delete(ws);
|
|
8128
|
-
}
|
|
8129
|
-
}
|
|
8130
|
-
}
|
|
8131
|
-
function liveReload(opts) {
|
|
8132
|
-
const r = new Router();
|
|
8133
|
-
r.ws("/__weifuwu/livereload", {
|
|
8134
|
-
open(ws) {
|
|
8135
|
-
clients2.add(ws);
|
|
8136
|
-
ws.on("close", () => clients2.delete(ws));
|
|
8137
|
-
ws.on("error", () => clients2.delete(ws));
|
|
8138
|
-
}
|
|
8139
|
-
});
|
|
8140
|
-
const watcher = chokidar.watch(opts.dirs, {
|
|
8141
|
-
ignored: /(^|[/\\])\.|node_modules|[/\\]\.weifuwu[/\\]/,
|
|
8142
|
-
ignoreInitial: true
|
|
8143
|
-
});
|
|
8144
|
-
watcher.on("change", (path2) => {
|
|
8145
|
-
if (!/\.tsx?$/.test(path2)) return;
|
|
8146
|
-
clearCompileCache();
|
|
8147
|
-
setTimeout(broadcastReload, 50);
|
|
8148
|
-
});
|
|
8149
|
-
r.close = () => {
|
|
8150
|
-
watcher.close();
|
|
8151
|
-
clients2.clear();
|
|
8152
|
-
};
|
|
8153
|
-
return r;
|
|
8154
|
-
}
|
|
8155
|
-
|
|
8156
|
-
// tailwind.ts
|
|
8157
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
8158
|
-
import { relative, resolve as resolve12 } from "node:path";
|
|
8159
|
-
var isDev2 = process.env.NODE_ENV !== "production";
|
|
8160
|
-
function tailwind(cssPath, scanDir2) {
|
|
8161
|
-
let compiledCss = "";
|
|
8162
|
-
let twWatcher = null;
|
|
8163
|
-
return async (req, ctx, next) => {
|
|
8164
|
-
const url = new URL(req.url);
|
|
8165
|
-
if (!compiledCss) {
|
|
8166
|
-
compiledCss = await compile(cssPath, scanDir2);
|
|
8167
|
-
}
|
|
8168
|
-
if (url.pathname === "/__wfw/style.css") {
|
|
8169
|
-
return new Response(compiledCss || "", {
|
|
8170
|
-
headers: { "content-type": "text/css; charset=utf-8" }
|
|
8171
|
-
});
|
|
8172
|
-
}
|
|
8173
|
-
ctx.compiledTailwindCss = compiledCss;
|
|
8174
|
-
if (isDev2 && !twWatcher) {
|
|
8175
|
-
twWatcher = watchFile(cssPath, () => {
|
|
8176
|
-
compiledCss = "";
|
|
8177
|
-
});
|
|
8178
|
-
}
|
|
8179
|
-
return next(req, ctx);
|
|
8180
|
-
};
|
|
8181
|
-
}
|
|
8182
|
-
async function compile(cssPath, scanDir2) {
|
|
8183
|
-
try {
|
|
8184
|
-
const inputFile = resolve12(cssPath);
|
|
8185
|
-
if (!existsSync5(inputFile)) {
|
|
8186
|
-
mkdirSync3(dirname4(inputFile), { recursive: true });
|
|
8187
|
-
writeFileSync3(inputFile, '@import "tailwindcss"\n', "utf-8");
|
|
8188
|
-
}
|
|
8189
|
-
const { default: tailwindPlugin } = await import("@tailwindcss/postcss");
|
|
8190
|
-
const { default: postcss } = await import("postcss");
|
|
8191
|
-
let src = readFileSync5(inputFile, "utf-8");
|
|
8192
|
-
const scanSource = scanDir2 ? relative(dirname4(inputFile), scanDir2) || "." : ".";
|
|
8193
|
-
const sourcePath = scanSource === "." ? "./" : `./${scanSource}/`;
|
|
8194
|
-
src = `@source "${sourcePath}";
|
|
8195
|
-
${src}`;
|
|
8196
|
-
const result = await postcss([tailwindPlugin()]).process(src, { from: inputFile });
|
|
8197
|
-
return result.css;
|
|
8198
|
-
} catch (err) {
|
|
8199
|
-
console.warn("Tailwind CSS processing failed:", err.message);
|
|
8200
|
-
return "";
|
|
8201
|
-
}
|
|
8202
|
-
}
|
|
8203
|
-
function dirname4(p) {
|
|
8204
|
-
return p.substring(0, p.lastIndexOf("/")) || "/";
|
|
8205
|
-
}
|
|
8206
|
-
function watchFile(path2, onChange) {
|
|
8207
|
-
let watcher = null;
|
|
8208
|
-
import("chokidar").then((chokidar2) => {
|
|
8209
|
-
watcher = chokidar2.default.watch(resolve12(path2), { persistent: false });
|
|
8210
|
-
watcher.on("change", onChange);
|
|
8211
|
-
});
|
|
8212
|
-
return watcher;
|
|
8213
|
-
}
|
|
8214
|
-
|
|
8215
8223
|
// not-found.ts
|
|
8216
8224
|
function notFound(path2) {
|
|
8217
8225
|
return async (req, ctx) => {
|
package/dist/live.d.ts
CHANGED
package/dist/tailwind.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default function RootLayout({ children }: { children: any }) {
|
|
2
|
+
return (
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charSet="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>Opencode Chat</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="__weifuwu_root">{children}</div>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
interface ToolCallEvent { toolName: string; input: unknown }
|
|
4
|
+
interface ToolResultEvent { toolName: string; output: unknown }
|
|
5
|
+
interface SessionItem { id: number; title: string; created_at?: string }
|
|
6
|
+
interface MessageItem { role: string; content: string; toolCalls?: ToolCallEvent[]; toolResults?: ToolResultEvent[] }
|
|
7
|
+
|
|
8
|
+
function formatDate(s: string) {
|
|
9
|
+
const d = new Date(s)
|
|
10
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function firstLine(text: string) {
|
|
14
|
+
return text.split('\n')[0].slice(0, 48) || 'Empty'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function Page() {
|
|
18
|
+
const [sessions, setSessions] = useState<SessionItem[]>([])
|
|
19
|
+
const [currentId, setCurrentId] = useState<number | null>(null)
|
|
20
|
+
const [messages, setMessages] = useState<MessageItem[]>([])
|
|
21
|
+
const [input, setInput] = useState('')
|
|
22
|
+
const [streaming, setStreaming] = useState('')
|
|
23
|
+
const [loading, setLoading] = useState(false)
|
|
24
|
+
const [error, setError] = useState('')
|
|
25
|
+
const abortRef = useRef<AbortController | null>(null)
|
|
26
|
+
const textRef = useRef('')
|
|
27
|
+
const bottomRef = useRef<HTMLDivElement>(null)
|
|
28
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
29
|
+
|
|
30
|
+
useEffect(() => { fetch('/opencode/sessions').then(r => r.json()).then(setSessions).catch(() => {}) }, [])
|
|
31
|
+
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages, streaming])
|
|
32
|
+
|
|
33
|
+
async function createSession() {
|
|
34
|
+
setError('')
|
|
35
|
+
const res = await fetch('/opencode/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) })
|
|
36
|
+
if (!res.ok) { setError('Failed'); return }
|
|
37
|
+
const s = await res.json()
|
|
38
|
+
setSessions(p => [s, ...p])
|
|
39
|
+
setCurrentId(s.id); setMessages([]); setStreaming(''); textRef.current = ''
|
|
40
|
+
setTimeout(() => inputRef.current?.focus(), 100)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function selectSession(id: number) {
|
|
44
|
+
abortRef.current?.abort()
|
|
45
|
+
setCurrentId(id); setStreaming(''); textRef.current = ''
|
|
46
|
+
const res = await fetch(`/opencode/sessions/${id}`)
|
|
47
|
+
if (!res.ok) { setError('Failed to load'); return }
|
|
48
|
+
const { messages: msgs } = await res.json() as any
|
|
49
|
+
setMessages(msgs.map((m: any) => ({
|
|
50
|
+
role: m.role, content: m.content || '',
|
|
51
|
+
toolCalls: Array.isArray(m.tool_calls) ? m.tool_calls : undefined,
|
|
52
|
+
toolResults: Array.isArray(m.tool_results) ? m.tool_results : undefined,
|
|
53
|
+
})))
|
|
54
|
+
setTimeout(() => inputRef.current?.focus(), 100)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function deleteSession(e: React.MouseEvent, id: number) {
|
|
58
|
+
e.stopPropagation()
|
|
59
|
+
fetch('/opencode/sessions/' + id, { method: 'DELETE' })
|
|
60
|
+
setSessions(p => p.filter(s => s.id !== id))
|
|
61
|
+
if (currentId === id) { setCurrentId(null); setMessages([]) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function sendMessage() {
|
|
65
|
+
const content = input.trim()
|
|
66
|
+
if (!content || !currentId || loading) return
|
|
67
|
+
setError('')
|
|
68
|
+
setMessages(p => [...p, { role: 'user', content }])
|
|
69
|
+
setInput(''); setLoading(true); setStreaming(''); textRef.current = ''
|
|
70
|
+
abortRef.current?.abort()
|
|
71
|
+
const controller = new AbortController()
|
|
72
|
+
abortRef.current = controller
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(`/opencode/sessions/${currentId}/message`, {
|
|
76
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
body: JSON.stringify({ content }), signal: controller.signal,
|
|
78
|
+
})
|
|
79
|
+
if (!res.ok) { setError('Request failed'); setLoading(false); return }
|
|
80
|
+
|
|
81
|
+
const reader = res.body!.getReader()
|
|
82
|
+
const decoder = new TextDecoder()
|
|
83
|
+
let buffer = ''
|
|
84
|
+
|
|
85
|
+
while (true) {
|
|
86
|
+
const { done, value } = await reader.read()
|
|
87
|
+
if (done) break
|
|
88
|
+
buffer += decoder.decode(value, { stream: true })
|
|
89
|
+
const lines = buffer.split('\n')
|
|
90
|
+
buffer = lines.pop() || ''
|
|
91
|
+
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
if (line.startsWith('event: ')) continue
|
|
94
|
+
if (!line.startsWith('data: ')) continue
|
|
95
|
+
try {
|
|
96
|
+
const d = JSON.parse(line.slice(6))
|
|
97
|
+
switch (d.type) {
|
|
98
|
+
case 'text-delta':
|
|
99
|
+
textRef.current += d.text || ''
|
|
100
|
+
setStreaming(textRef.current)
|
|
101
|
+
break
|
|
102
|
+
case 'tool-call':
|
|
103
|
+
setMessages(p => {
|
|
104
|
+
const last = p[p.length - 1]
|
|
105
|
+
if (last?.role === 'assistant') {
|
|
106
|
+
const tcs = last.toolCalls || []
|
|
107
|
+
return [...p.slice(0, -1), { ...last, toolCalls: [...tcs, { toolName: d.toolName, input: d.input }] }]
|
|
108
|
+
}
|
|
109
|
+
return [...p, { role: 'assistant', content: '', toolCalls: [{ toolName: d.toolName, input: d.input }] }]
|
|
110
|
+
})
|
|
111
|
+
break
|
|
112
|
+
case 'tool-result':
|
|
113
|
+
setMessages(p => {
|
|
114
|
+
const last = p[p.length - 1]
|
|
115
|
+
if (last?.role === 'assistant') {
|
|
116
|
+
const trs = last.toolResults || []
|
|
117
|
+
return [...p.slice(0, -1), { ...last, toolResults: [...trs, { toolName: d.toolName, output: d.output }] }]
|
|
118
|
+
}
|
|
119
|
+
return [...p, { role: 'assistant', content: '', toolResults: [{ toolName: d.toolName, output: d.output }] }]
|
|
120
|
+
})
|
|
121
|
+
break
|
|
122
|
+
case 'finish':
|
|
123
|
+
const finalContent = textRef.current
|
|
124
|
+
setMessages(p => {
|
|
125
|
+
const last = p[p.length - 1]
|
|
126
|
+
if (last?.role === 'assistant' && !last.content && finalContent) {
|
|
127
|
+
return [...p.slice(0, -1), { ...last, content: finalContent }]
|
|
128
|
+
}
|
|
129
|
+
if (last?.role === 'assistant' && last.content) return p
|
|
130
|
+
return [...p, { role: 'assistant', content: finalContent }]
|
|
131
|
+
})
|
|
132
|
+
setStreaming(''); textRef.current = ''; setLoading(false)
|
|
133
|
+
setTimeout(() => inputRef.current?.focus(), 50)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (e: any) {
|
|
140
|
+
if (e.name !== 'AbortError') setError(e.message || 'Error')
|
|
141
|
+
}
|
|
142
|
+
setLoading(false)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="flex h-screen font-sans antialiased bg-zinc-950 text-zinc-100">
|
|
147
|
+
{/* Sidebar */}
|
|
148
|
+
<aside className="w-64 flex flex-col border-r border-zinc-800 bg-zinc-900/50 shrink-0">
|
|
149
|
+
<div className="p-3 border-b border-zinc-800">
|
|
150
|
+
<button onClick={createSession}
|
|
151
|
+
className="w-full flex items-center justify-center gap-2 py-2 px-3 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded-lg text-sm transition-colors cursor-pointer">
|
|
152
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4"/></svg>
|
|
153
|
+
New Chat
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
<nav className="flex-1 overflow-y-auto p-2 space-y-0.5">
|
|
157
|
+
{sessions.length === 0 && (
|
|
158
|
+
<div className="text-xs text-zinc-600 text-center py-8">No sessions yet</div>
|
|
159
|
+
)}
|
|
160
|
+
{sessions.map(s => (
|
|
161
|
+
<div key={s.id}
|
|
162
|
+
onClick={() => selectSession(s.id)}
|
|
163
|
+
className={`group flex items-center gap-2 p-2 rounded-lg cursor-pointer text-sm transition-colors ${
|
|
164
|
+
currentId === s.id ? 'bg-zinc-700/60 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800/60 hover:text-zinc-200'
|
|
165
|
+
}`}>
|
|
166
|
+
<svg className="w-4 h-4 shrink-0 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg>
|
|
167
|
+
<span className="truncate flex-1">{s.title || `Session ${s.id}`}</span>
|
|
168
|
+
<button onClick={e => deleteSession(e, s.id)}
|
|
169
|
+
className="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-zinc-600 transition-all cursor-pointer">
|
|
170
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
))}
|
|
174
|
+
</nav>
|
|
175
|
+
</aside>
|
|
176
|
+
|
|
177
|
+
{/* Main */}
|
|
178
|
+
<main className="flex-1 flex flex-col min-w-0">
|
|
179
|
+
{/* Header */}
|
|
180
|
+
{currentId && (
|
|
181
|
+
<header className="flex items-center gap-2 px-5 py-2.5 border-b border-zinc-800 bg-zinc-900/30">
|
|
182
|
+
<svg className="w-4 h-4 text-emerald-500" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
|
183
|
+
<span className="text-xs text-zinc-500 font-mono">opencode</span>
|
|
184
|
+
<span className="text-xs text-zinc-600 mx-1">/</span>
|
|
185
|
+
<span className="text-sm text-zinc-300 truncate">{sessions.find(s => s.id === currentId)?.title || `Session ${currentId}`}</span>
|
|
186
|
+
</header>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{/* Messages */}
|
|
190
|
+
<div className="flex-1 overflow-y-auto">
|
|
191
|
+
<div className="max-w-3xl mx-auto px-4 py-6 space-y-4">
|
|
192
|
+
{!currentId && (
|
|
193
|
+
<div className="flex flex-col items-center justify-center h-[70vh] text-zinc-600">
|
|
194
|
+
<svg className="w-12 h-12 mb-4 opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg>
|
|
195
|
+
<p className="text-sm mb-2">Select a session or create a new one</p>
|
|
196
|
+
<button onClick={createSession} className="mt-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg text-sm transition-colors cursor-pointer">
|
|
197
|
+
+ New Chat
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{messages.map((m, i) => (
|
|
203
|
+
<div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
204
|
+
<div className={`max-w-[75%] rounded-2xl px-4 py-2.5 leading-relaxed whitespace-pre-wrap text-sm ${
|
|
205
|
+
m.role === 'user'
|
|
206
|
+
? 'bg-indigo-600 text-white rounded-br-md'
|
|
207
|
+
: 'bg-zinc-800/80 text-zinc-200 rounded-bl-md border border-zinc-700/50'
|
|
208
|
+
}`}>
|
|
209
|
+
{m.content || <span className="text-zinc-500 italic">No response</span>}
|
|
210
|
+
{m.toolCalls?.map((tc, j) => (
|
|
211
|
+
<details key={j} className="mt-2 rounded-lg overflow-hidden bg-black/20 border border-zinc-700/50">
|
|
212
|
+
<summary className="px-3 py-1.5 text-xs text-zinc-400 cursor-pointer hover:text-zinc-200 select-none flex items-center gap-1.5">
|
|
213
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
|
214
|
+
{tc.toolName}
|
|
215
|
+
</summary>
|
|
216
|
+
<pre className="px-3 py-2 text-xs text-zinc-400 overflow-x-auto">{JSON.stringify(tc.input, null, 2)}</pre>
|
|
217
|
+
</details>
|
|
218
|
+
))}
|
|
219
|
+
{m.toolResults?.map((tr, j) => (
|
|
220
|
+
<details key={j} className="mt-1.5 rounded-lg overflow-hidden bg-black/20 border border-zinc-700/50">
|
|
221
|
+
<summary className="px-3 py-1.5 text-xs text-zinc-400 cursor-pointer hover:text-zinc-200 select-none flex items-center gap-1.5">
|
|
222
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
223
|
+
{tr.toolName} result
|
|
224
|
+
</summary>
|
|
225
|
+
<pre className="px-3 py-2 text-xs text-zinc-400 overflow-x-auto max-h-48">{JSON.stringify(tr.output, null, 2)}</pre>
|
|
226
|
+
</details>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
))}
|
|
231
|
+
|
|
232
|
+
{streaming && (
|
|
233
|
+
<div className="flex justify-start">
|
|
234
|
+
<div className="max-w-[75%] rounded-2xl px-4 py-2.5 bg-zinc-800/80 text-zinc-200 rounded-bl-md border border-zinc-700/50 leading-relaxed whitespace-pre-wrap text-sm">
|
|
235
|
+
{streaming}<span className="inline-block w-1.5 h-4 bg-indigo-400 ml-0.5 animate-pulse rounded-sm" />
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{loading && !streaming && (
|
|
241
|
+
<div className="flex justify-start">
|
|
242
|
+
<div className="flex items-center gap-2 px-4 py-3 bg-zinc-800/60 rounded-2xl rounded-bl-md border border-zinc-700/40">
|
|
243
|
+
<div className="flex gap-1">
|
|
244
|
+
<span className="w-2 h-2 bg-zinc-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
|
245
|
+
<span className="w-2 h-2 bg-zinc-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
|
246
|
+
<span className="w-2 h-2 bg-zinc-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
{error && (
|
|
253
|
+
<div className="flex justify-center">
|
|
254
|
+
<div className="flex items-center gap-2 px-4 py-2 bg-red-900/30 text-red-400 rounded-lg text-xs border border-red-800/40">
|
|
255
|
+
<svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
256
|
+
{error}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
<div ref={bottomRef} />
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Input */}
|
|
266
|
+
<div className="border-t border-zinc-800 bg-zinc-900/50">
|
|
267
|
+
<div className="max-w-3xl mx-auto px-4 py-3">
|
|
268
|
+
<div className="flex items-end gap-2 bg-zinc-800/80 rounded-xl border border-zinc-700/50 px-3 py-2 focus-within:border-zinc-500 transition-colors">
|
|
269
|
+
<input ref={inputRef}
|
|
270
|
+
value={input}
|
|
271
|
+
onChange={e => setInput(e.target.value)}
|
|
272
|
+
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage() } }}
|
|
273
|
+
placeholder={currentId ? 'Type a message...' : 'Create a session first'}
|
|
274
|
+
disabled={!currentId || loading}
|
|
275
|
+
className="flex-1 bg-transparent text-sm text-zinc-100 placeholder-zinc-500 outline-none resize-none disabled:opacity-40"
|
|
276
|
+
/>
|
|
277
|
+
<button onClick={sendMessage} disabled={!currentId || loading || !input.trim()}
|
|
278
|
+
className="flex items-center justify-center p-1.5 rounded-lg bg-indigo-600 hover:bg-indigo-500 disabled:bg-zinc-700 disabled:text-zinc-500 text-white transition-colors cursor-pointer disabled:cursor-default shrink-0">
|
|
279
|
+
{loading ? (
|
|
280
|
+
<svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/></svg>
|
|
281
|
+
) : (
|
|
282
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19V5m0 0l-7 7m7-7l7 7"/></svg>
|
|
283
|
+
)}
|
|
284
|
+
</button>
|
|
285
|
+
</div>
|
|
286
|
+
{currentId && (
|
|
287
|
+
<p className="text-[10px] text-zinc-600 text-center mt-1.5">Enter to send</p>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</main>
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weifuwu",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.10",
|
|
4
4
|
"description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"dist/",
|
|
16
16
|
"cli.ts",
|
|
17
|
+
"opencode/ui/",
|
|
17
18
|
"README.md",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|