shine-code-submit 0.2.0
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/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +10 -0
- package/LICENSE +21 -0
- package/README.md +217 -0
- package/bin/launcher.cjs +35 -0
- package/bun.lock +45 -0
- package/dist/install.cjs +8 -0
- package/hooks/hooks.json +81 -0
- package/package.json +68 -0
- package/src/cli/main.ts +128 -0
- package/src/daemon/auth.ts +7 -0
- package/src/daemon/bus.ts +25 -0
- package/src/daemon/git.ts +118 -0
- package/src/daemon/logger.ts +66 -0
- package/src/daemon/main.ts +95 -0
- package/src/daemon/server.ts +208 -0
- package/src/daemon/spool-consumer.ts +52 -0
- package/src/daemon/stats.ts +33 -0
- package/src/daemon/store.ts +147 -0
- package/src/daemon/token-cache.ts +35 -0
- package/src/daemon/transcript.ts +130 -0
- package/src/daemon/ui-assets.ts +4 -0
- package/src/daemon/ui.ts +34 -0
- package/src/daemon/ws.ts +54 -0
- package/src/hook/main.ts +154 -0
- package/src/install/bun.ts +97 -0
- package/src/install/deploy.ts +78 -0
- package/src/install/json-safe.ts +43 -0
- package/src/install/main.ts +167 -0
- package/src/install/paths.ts +28 -0
- package/src/install/register.ts +108 -0
- package/src/shared/config.ts +77 -0
- package/src/shared/daemonctl.ts +110 -0
- package/src/shared/id.ts +19 -0
- package/src/shared/paths.ts +23 -0
- package/src/shared/pidfile.ts +32 -0
- package/src/shared/spool.ts +60 -0
- package/src/shared/types.ts +116 -0
- package/ui/.build/app.js +76 -0
- package/ui/app.tsx +31 -0
- package/ui/components/App.tsx +55 -0
- package/ui/components/CommitsModule.tsx +25 -0
- package/ui/components/CommitsView.tsx +84 -0
- package/ui/components/Conversation.tsx +101 -0
- package/ui/components/DiffBlock.tsx +64 -0
- package/ui/components/EventDetail.tsx +8 -0
- package/ui/components/EventItem.tsx +24 -0
- package/ui/components/EventList.tsx +22 -0
- package/ui/components/EventsModule.tsx +123 -0
- package/ui/components/Header.tsx +15 -0
- package/ui/components/Icon.tsx +114 -0
- package/ui/components/Markdown.tsx +13 -0
- package/ui/components/Message.tsx +60 -0
- package/ui/components/OverviewModule.tsx +86 -0
- package/ui/components/SessionDetail.tsx +51 -0
- package/ui/components/SessionTree.tsx +94 -0
- package/ui/components/SessionsModule.tsx +39 -0
- package/ui/components/SessionsPanel.tsx +15 -0
- package/ui/components/SideNav.tsx +46 -0
- package/ui/components/Splitter.tsx +23 -0
- package/ui/components/StatsModule.tsx +133 -0
- package/ui/components/Status.tsx +66 -0
- package/ui/components/SummaryView.tsx +83 -0
- package/ui/components/SystemModule.tsx +38 -0
- package/ui/components/ToolCard.tsx +57 -0
- package/ui/hooks/useAllCommits.ts +59 -0
- package/ui/hooks/useApi.ts +13 -0
- package/ui/hooks/useCommits.ts +45 -0
- package/ui/hooks/useConversation.ts +43 -0
- package/ui/hooks/useEvents.ts +45 -0
- package/ui/hooks/useSplitter.ts +43 -0
- package/ui/hooks/useStatsPolling.ts +35 -0
- package/ui/hooks/useWebSocket.ts +38 -0
- package/ui/index.html +13 -0
- package/ui/lib/aggregate.ts +81 -0
- package/ui/lib/diff.ts +45 -0
- package/ui/lib/export.ts +45 -0
- package/ui/lib/format.ts +100 -0
- package/ui/lib/util.ts +106 -0
- package/ui/state/AppContext.tsx +65 -0
- package/ui/style.css +849 -0
- package/ui/types.ts +46 -0
package/ui/lib/export.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// 导出工具(从原 app.js 搬运)。
|
|
2
|
+
import type { TranscriptMessage } from "../types";
|
|
3
|
+
|
|
4
|
+
export function download(name: string, content: string, type = "text/plain"): void {
|
|
5
|
+
const blob = new Blob([content], { type: type + ";charset=utf-8" });
|
|
6
|
+
const url = URL.createObjectURL(blob);
|
|
7
|
+
const a = document.createElement("a");
|
|
8
|
+
a.href = url;
|
|
9
|
+
a.download = name;
|
|
10
|
+
document.body.appendChild(a);
|
|
11
|
+
a.click();
|
|
12
|
+
a.remove();
|
|
13
|
+
URL.revokeObjectURL(url);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 对话消息导出为 Markdown。 */
|
|
17
|
+
export function messagesToMd(messages: TranscriptMessage[], sid: string): string {
|
|
18
|
+
const lines: string[] = [
|
|
19
|
+
"# 对话导出", "",
|
|
20
|
+
"session: " + sid,
|
|
21
|
+
"导出时间: " + new Date().toLocaleString(),
|
|
22
|
+
"消息数: " + messages.length,
|
|
23
|
+
"", "---", "",
|
|
24
|
+
];
|
|
25
|
+
for (const m of messages) {
|
|
26
|
+
const when = m.ts ? new Date(m.ts).toLocaleString() : "";
|
|
27
|
+
if (m.role === "tool") {
|
|
28
|
+
lines.push("### 🔧 " + (m.toolName || "工具") + " · 结果" + (when ? " · " + when : ""), "", "```", m.text || "", "```", "");
|
|
29
|
+
} else if (m.role === "user") {
|
|
30
|
+
lines.push("## 用户" + (when ? " · " + when : ""), "", m.text || "", "");
|
|
31
|
+
} else {
|
|
32
|
+
lines.push("## Claude" + (when ? " · " + when : ""), "");
|
|
33
|
+
if (m.thinking) lines.push("<details><summary>思考过程</summary>", "", m.thinking, "", "</details>", "");
|
|
34
|
+
lines.push(m.text || "", "");
|
|
35
|
+
if (m.tools && m.tools.length) {
|
|
36
|
+
lines.push("<details><summary>工具调用 (" + m.tools.length + ")</summary>", "");
|
|
37
|
+
for (const t of m.tools) {
|
|
38
|
+
lines.push("- **" + t.name + "**:", " ```json", JSON.stringify(t.input, null, 2).replace(/\n/g, "\n "), " ```", "");
|
|
39
|
+
}
|
|
40
|
+
lines.push("</details>", "");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
package/ui/lib/format.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// 事件摘要 / 详情 / 工具摘要等格式化(从原 app.js 搬运)。
|
|
2
|
+
// 返回 HTML 字符串的函数(formatDetail/jsonBlock/textBlock/section/renderContentBlock)
|
|
3
|
+
// 在组件里用 dangerouslySetInnerHTML 渲染:内部只产生 span/class 标记,来源数据已 escapeHtml。
|
|
4
|
+
import type { HookEvent, Payload } from "../types";
|
|
5
|
+
import { brief, escapeHtml, safeJson } from "./util";
|
|
6
|
+
|
|
7
|
+
/** 事件流一行摘要(工具名 + prompt/回复/工具输入片段)。 */
|
|
8
|
+
export function eventSummary(ev: HookEvent): string {
|
|
9
|
+
const p = (ev.payload ?? {}) as Payload;
|
|
10
|
+
const tool = typeof p.tool_name === "string" && p.tool_name ? ` ${p.tool_name}` : "";
|
|
11
|
+
let extra = "";
|
|
12
|
+
if (typeof p.prompt === "string") extra = " " + brief(p.prompt);
|
|
13
|
+
else if (typeof p.last_assistant_message === "string") extra = " " + brief(p.last_assistant_message);
|
|
14
|
+
else if (p.tool_input != null) extra = " " + brief(safeJson(p.tool_input));
|
|
15
|
+
return `${tool}${extra}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 工具调用参数的简短摘要(用于卡片副标题)。 */
|
|
19
|
+
export function toolParamSummary(name: string, input: unknown): string {
|
|
20
|
+
const g = (k: string): string => {
|
|
21
|
+
if (input && typeof input === "object") {
|
|
22
|
+
const v = (input as Record<string, unknown>)[k];
|
|
23
|
+
return v != null ? String(v) : "";
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
};
|
|
27
|
+
switch (name) {
|
|
28
|
+
case "Bash": return g("command");
|
|
29
|
+
case "Read":
|
|
30
|
+
case "Edit":
|
|
31
|
+
case "Write": return g("file_path");
|
|
32
|
+
case "Grep":
|
|
33
|
+
case "Glob": return g("pattern");
|
|
34
|
+
case "WebSearch": return g("query");
|
|
35
|
+
case "WebFetch": return g("url");
|
|
36
|
+
case "TaskCreate": return g("subject") || g("description");
|
|
37
|
+
case "TaskUpdate": return "#" + g("taskId") + " → " + g("status");
|
|
38
|
+
case "TaskGet": return "#" + g("taskId");
|
|
39
|
+
case "TaskList": return "";
|
|
40
|
+
case "Agent": return g("description");
|
|
41
|
+
default: return safeJson(input).slice(0, 80);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const TOOL_ICONS: Record<string, string> = {
|
|
46
|
+
Bash: "▶", Read: "📄", Edit: "✎", Write: "✎", Grep: "🔎", Glob: "🗂",
|
|
47
|
+
WebSearch: "🌐", WebFetch: "🌐", TaskCreate: "📌", TaskUpdate: "📌",
|
|
48
|
+
TaskGet: "📌", TaskList: "📌", Agent: "🤖",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function toolIcon(name: string): string {
|
|
52
|
+
return TOOL_ICONS[name] ?? "🛠";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- 以下返回 HTML 片段,组件用 dangerouslySetInnerHTML 渲染 ----
|
|
56
|
+
|
|
57
|
+
export function section(label: string, content: string): string {
|
|
58
|
+
return `<div class="detail-section"><div class="detail-label">${label}</div><div class="detail-content">${content}</div></div>`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function textBlock(s: string): string {
|
|
62
|
+
return `<div class="detail-text">${escapeHtml(s)}</div>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function jsonBlock(v: unknown): string {
|
|
66
|
+
let s = escapeHtml(safeJson(v));
|
|
67
|
+
s = s.replace(/(".*?")(\s*:)/g, '<span class="j-key">$1</span>$2');
|
|
68
|
+
s = s.replace(/(:\s*)(".*?")(?=[,\n]|$)/g, '$1<span class="j-str">$2</span>');
|
|
69
|
+
s = s.replace(/(:\s*)(-?\d+(?:\.\d+)?)/g, '$1<span class="j-num">$2</span>');
|
|
70
|
+
s = s.replace(/(:\s*)(true|false|null)/g, '$1<span class="j-bool">$2</span>');
|
|
71
|
+
return `<div class="detail-json">${s}</div>`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 事件详情面板完整 HTML。 */
|
|
75
|
+
export function formatDetail(payload: unknown): string {
|
|
76
|
+
const p = (payload ?? {}) as Payload;
|
|
77
|
+
const parts: string[] = [];
|
|
78
|
+
if (typeof p.prompt === "string") parts.push(section("用户提问", textBlock(p.prompt)));
|
|
79
|
+
if (typeof p.last_assistant_message === "string") parts.push(section("Claude 回复", textBlock(p.last_assistant_message)));
|
|
80
|
+
if (typeof p.tool_name === "string") {
|
|
81
|
+
parts.push(`<div class="detail-section"><div class="detail-label">工具</div><div class="detail-content"><code class="tool-name">${escapeHtml(p.tool_name)}</code></div></div>`);
|
|
82
|
+
}
|
|
83
|
+
if (p.tool_input != null) parts.push(section("工具输入", jsonBlock(p.tool_input)));
|
|
84
|
+
if (p.tool_response != null) parts.push(section("工具结果", jsonBlock(p.tool_response)));
|
|
85
|
+
if (typeof p.reason === "string") parts.push(section("原因", textBlock(p.reason)));
|
|
86
|
+
if (typeof p.source === "string") parts.push(section("来源", textBlock(p.source)));
|
|
87
|
+
if (!parts.length) parts.push(section("原始数据", jsonBlock(payload)));
|
|
88
|
+
return parts.join("");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Write 工具内容预览(前 50 行)。 */
|
|
92
|
+
export function renderContentBlock(content: unknown): string {
|
|
93
|
+
const all = String(content || "").split("\n");
|
|
94
|
+
const shown = all.slice(0, 50);
|
|
95
|
+
const rows = shown.map((l) => `<div class="diff-line ctx"><span class="diff-text">${escapeHtml(l)}</span></div>`);
|
|
96
|
+
if (all.length > shown.length) {
|
|
97
|
+
rows.push(`<div class="diff-folds">⋯ 共 ${all.length} 行,前 ${shown.length} 行</div>`);
|
|
98
|
+
}
|
|
99
|
+
return `<div class="diff-block">${rows.join("")}</div>`;
|
|
100
|
+
}
|
package/ui/lib/util.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// 字符串 / 格式工具(从原 app.js 搬运,改 TS 签名)。
|
|
2
|
+
import type { TokenUsage, TranscriptMessage } from "../types";
|
|
3
|
+
|
|
4
|
+
const HTML_ESCAPES: Record<string, string> = {
|
|
5
|
+
"&": "&",
|
|
6
|
+
"<": "<",
|
|
7
|
+
">": ">",
|
|
8
|
+
'"': """,
|
|
9
|
+
"'": "'",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function escapeHtml(s: unknown): string {
|
|
13
|
+
return String(s).replace(/[&<>"']/g, (c) => HTML_ESCAPES[c] ?? c);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function fmtTime(ts: number): string {
|
|
17
|
+
const d = new Date(ts);
|
|
18
|
+
return d.toLocaleTimeString() + "." + String(d.getMilliseconds()).padStart(3, "0");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 带日期的时间(提交跨天,需日期):MM-DD HH:MM。 */
|
|
22
|
+
export function fmtDateTime(ts: number): string {
|
|
23
|
+
const d = new Date(ts);
|
|
24
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
25
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
26
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
27
|
+
const mi = String(d.getMinutes()).padStart(2, "0");
|
|
28
|
+
return `${mm}-${dd} ${hh}:${mi}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function brief(s: unknown, n = 80): string {
|
|
32
|
+
const t = String(s).replace(/\s+/g, " ").trim();
|
|
33
|
+
return t.length > n ? t.slice(0, n) + "…" : t;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function safeJson(v: unknown): string {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(v, null, 2);
|
|
39
|
+
} catch {
|
|
40
|
+
return String(v);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 紧凑数字:1234 → "1.2k",1234567 → "1.2M",<1000 原样。 */
|
|
45
|
+
export function fmtTokens(n: number): string {
|
|
46
|
+
if (!Number.isFinite(n) || n < 1000) return String(n);
|
|
47
|
+
if (n < 1_000_000) return trimZero((n / 1000).toFixed(1)) + "k";
|
|
48
|
+
return trimZero((n / 1_000_000).toFixed(1)) + "M";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** token 用量简写:↑输入 ↓输出(cache 明细放 title)。无值返回空串。 */
|
|
52
|
+
export function fmtUsage(u?: Pick<TokenUsage, "input" | "output"> | null): string {
|
|
53
|
+
if (!u) return "";
|
|
54
|
+
return `↑${fmtTokens(u.input)} ↓${fmtTokens(u.output)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 完整 token 用量(input/output/cacheCreation/cacheRead),用于 title 提示。 */
|
|
58
|
+
export function fmtUsageFull(u?: TokenUsage | null): string {
|
|
59
|
+
if (!u) return "";
|
|
60
|
+
return `输入 ${u.input} · 输出 ${u.output} · 缓存写 ${u.cacheCreation} · 缓存读 ${u.cacheRead}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function trimZero(s: string): string {
|
|
64
|
+
return s.replace(/\.0$/, "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 累加 assistant 消息的 usage(对话视图会话级汇总)。 */
|
|
68
|
+
export function sumUsage(messages: TranscriptMessage[]): TokenUsage {
|
|
69
|
+
const total: TokenUsage = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
70
|
+
for (const m of messages) {
|
|
71
|
+
if (m.usage) {
|
|
72
|
+
total.input += m.usage.input;
|
|
73
|
+
total.output += m.usage.output;
|
|
74
|
+
total.cacheCreation += m.usage.cacheCreation;
|
|
75
|
+
total.cacheRead += m.usage.cacheRead;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return total;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 累加若干 TokenUsage(可为 null/undefined,如未被 enrich 的旧会话),返回总量与有效条数。
|
|
82
|
+
* 顶栏全局 token 用:sessions 里仅最近 N 个有 tokenTotal,count 反映实际覆盖的会话数。 */
|
|
83
|
+
export function sumTokenUsage(
|
|
84
|
+
usages: (TokenUsage | null | undefined)[],
|
|
85
|
+
): { total: TokenUsage; count: number } {
|
|
86
|
+
const total: TokenUsage = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
87
|
+
let count = 0;
|
|
88
|
+
for (const u of usages) {
|
|
89
|
+
if (u) {
|
|
90
|
+
total.input += u.input;
|
|
91
|
+
total.output += u.output;
|
|
92
|
+
total.cacheCreation += u.cacheCreation;
|
|
93
|
+
total.cacheRead += u.cacheRead;
|
|
94
|
+
count++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { total, count };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 路径取末段作项目名(汇总页显示用):"/a/b/shine-code-submit" → "shine-code-submit"。 */
|
|
101
|
+
export function shortDir(p: string): string {
|
|
102
|
+
if (!p) return "";
|
|
103
|
+
const t = p.replace(/[\\/]+$/, "");
|
|
104
|
+
const i = Math.max(t.lastIndexOf("/"), t.lastIndexOf("\\"));
|
|
105
|
+
return i >= 0 ? t.slice(i + 1) : t;
|
|
106
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// 全局状态聚合:只保留跨模块共享的导航态 + 全局轮询数据。
|
|
2
|
+
// 各视图数据(events/messages/commits)下沉到模块组件自管 hook,互不覆盖。
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useState,
|
|
8
|
+
type Dispatch,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
type SetStateAction,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { useApi } from "../hooks/useApi";
|
|
13
|
+
import { useStatsPolling } from "../hooks/useStatsPolling";
|
|
14
|
+
import type { ModuleId, SessionSummary, StatsResponse } from "../types";
|
|
15
|
+
|
|
16
|
+
export interface AppContextValue {
|
|
17
|
+
token: string;
|
|
18
|
+
// 全局轮询
|
|
19
|
+
stats: StatsResponse | null;
|
|
20
|
+
sessions: SessionSummary[];
|
|
21
|
+
// 导航
|
|
22
|
+
selectedSessionId: string | null; // 仅「会话」模块详情用
|
|
23
|
+
activeModule: ModuleId;
|
|
24
|
+
navCollapsed: boolean;
|
|
25
|
+
// setters
|
|
26
|
+
setSelectedSessionId: Dispatch<SetStateAction<string | null>>;
|
|
27
|
+
selectModule: (m: ModuleId) => void;
|
|
28
|
+
setNavCollapsed: Dispatch<SetStateAction<boolean>>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const AppContext = createContext<AppContextValue | null>(null);
|
|
32
|
+
|
|
33
|
+
export function AppProvider({ token, children }: { token: string; children: ReactNode }) {
|
|
34
|
+
const [stats, setStats] = useState<StatsResponse | null>(null);
|
|
35
|
+
const [sessions, setSessions] = useState<SessionSummary[]>([]);
|
|
36
|
+
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null);
|
|
37
|
+
const [activeModule, setActiveModule] = useState<ModuleId>("overview");
|
|
38
|
+
const [navCollapsed, setNavCollapsed] = useState(false);
|
|
39
|
+
|
|
40
|
+
const api = useApi(token);
|
|
41
|
+
useStatsPolling(api, setStats, setSessions);
|
|
42
|
+
|
|
43
|
+
const selectModule = useCallback((m: ModuleId) => {
|
|
44
|
+
setActiveModule(m);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const value: AppContextValue = {
|
|
48
|
+
token,
|
|
49
|
+
stats,
|
|
50
|
+
sessions,
|
|
51
|
+
selectedSessionId,
|
|
52
|
+
activeModule,
|
|
53
|
+
navCollapsed,
|
|
54
|
+
setSelectedSessionId,
|
|
55
|
+
selectModule,
|
|
56
|
+
setNavCollapsed,
|
|
57
|
+
};
|
|
58
|
+
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useApp(): AppContextValue {
|
|
62
|
+
const ctx = useContext(AppContext);
|
|
63
|
+
if (!ctx) throw new Error("useApp must be used within <AppProvider>");
|
|
64
|
+
return ctx;
|
|
65
|
+
}
|