pi-workspace 0.1.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/.env.example ADDED
@@ -0,0 +1 @@
1
+ PORT=8787
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # pi-gui
2
+
3
+ `pi-gui` is a web-based agent conversation console built on the
4
+ `@earendil-works/pi-coding-agent` SDK. It provides a browser UI while keeping
5
+ model credentials and the Pi agent session runtime on the server.
6
+
7
+ ## Features
8
+
9
+ - Online chat interface for Pi-powered agent conversations
10
+ - Server-side `createAgentSession()` integration from `@earendil-works/pi-coding-agent`
11
+ - Streaming assistant responses over Server-Sent Events
12
+ - Model selector for common OpenAI, Anthropic, Google, Mistral, and Command Code models
13
+ - Editable system prompt
14
+ - Browser-side conversation transcript persistence
15
+ - Server-side model auth through local Pi and Command Code login state
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ npm install
21
+ npm exec -- pi-gui
22
+ ```
23
+
24
+ If you already use Pi locally, the server reads your existing Pi auth from
25
+ `~/.pi/agent/auth.json` and custom models from `~/.pi/agent/models.json`.
26
+ It also detects Command Code CLI login credentials from
27
+ `~/.commandcode/auth.json`.
28
+
29
+ When Command Code auth is present, the server fetches live models from
30
+ `https://api.commandcode.ai/provider/v1/models` and registers them under the
31
+ `commandcode` provider.
32
+
33
+ If you want to customize the server port, you can still create a local `.env`
34
+ from `.env.example` and change `PORT`.
35
+
36
+ Start the packaged service:
37
+
38
+ ```bash
39
+ npm exec -- pi-gui
40
+ ```
41
+
42
+ Open <http://127.0.0.1:8787>.
43
+
44
+ Useful CLI commands:
45
+
46
+ ```bash
47
+ npm exec -- pi-gui # start the built service
48
+ npm exec -- pi-gui build # build client + server bundles
49
+ npm exec -- pi-gui --help # show all options
50
+ ```
51
+
52
+ ## npm Packaging
53
+
54
+ `pi-gui` is set up as a publishable npm CLI package:
55
+
56
+ - `npm publish` runs `prepack`, which builds `dist/client` and `dist-server`
57
+ - the published tarball includes only the CLI entrypoint and built runtime assets
58
+ - `pi-gui` starts the bundled production server directly from `dist-server/index.mjs`
59
+
60
+ ## Production
61
+
62
+ ```bash
63
+ npm run build
64
+ npm start
65
+ ```
66
+
67
+ The production server listens on `PORT` or `8787` and serves both the API and
68
+ the built frontend.
69
+
70
+ ## Architecture
71
+
72
+ - `src/` contains the React conversation UI.
73
+ - `server/index.ts` owns the Pi Coding Agent SDK integration.
74
+ - The frontend sends only the latest user prompt and session metadata.
75
+ - The backend keeps a per-browser-session `AgentSession` in memory and streams
76
+ `message_update` deltas back to the browser.
77
+ - `AuthStorage.create()` and `ModelRegistry.create()` load the same local auth
78
+ and model registry that the Pi CLI uses.
79
+
80
+ By default the server starts Pi sessions with `noTools: "all"` so the online
81
+ chat cannot execute shell or file mutation tools. Add a deliberate tool allowlist
82
+ only after adding authentication and permission controls.
83
+
84
+ ## Attribution
85
+
86
+ This project is customized from the public Pi ecosystem by Earendil Works:
87
+ <https://github.com/earendil-works/pi>.
package/bin/pi-gui.mjs ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import { dirname, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const projectRoot = resolve(__dirname, "..");
10
+ const cliName = "pi-gui";
11
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
12
+
13
+ function printHelp() {
14
+ console.log(
15
+ [
16
+ `${cliName} - start the bundled pi-gui service`,
17
+ "",
18
+ "Usage:",
19
+ ` ${cliName} Start the built service`,
20
+ ` ${cliName} start Start the built production server`,
21
+ ` ${cliName} build Build client and server bundles`,
22
+ ` ${cliName} --help Show this help message`,
23
+ "",
24
+ "Options:",
25
+ " --port <number> Override PORT for the service"
26
+ ].join("\n")
27
+ );
28
+ }
29
+
30
+ function parseArgs(argv) {
31
+ let command = "start";
32
+ let port;
33
+
34
+ for (let index = 0; index < argv.length; index += 1) {
35
+ const arg = argv[index];
36
+
37
+ if (arg === "--help" || arg === "-h") {
38
+ command = "help";
39
+ continue;
40
+ }
41
+
42
+ if (arg === "--port") {
43
+ const value = argv[index + 1];
44
+ if (!value || value.startsWith("-")) {
45
+ throw new Error("Missing value for --port");
46
+ }
47
+ port = value;
48
+ index += 1;
49
+ continue;
50
+ }
51
+
52
+ if (arg === "start" || arg === "build" || arg === "help") {
53
+ command = arg;
54
+ continue;
55
+ }
56
+
57
+ throw new Error(`Unknown argument: ${arg}`);
58
+ }
59
+
60
+ return { command, port };
61
+ }
62
+
63
+ function run(command, args, env = {}) {
64
+ return new Promise((resolvePromise, reject) => {
65
+ const child = spawn(command, args, {
66
+ cwd: projectRoot,
67
+ stdio: "inherit",
68
+ env: { ...process.env, ...env }
69
+ });
70
+
71
+ child.on("error", reject);
72
+ child.on("exit", (code) => {
73
+ if (code === 0) {
74
+ resolvePromise();
75
+ return;
76
+ }
77
+
78
+ reject(new Error(`${command} ${args.join(" ")} exited with code ${code ?? 1}`));
79
+ });
80
+ });
81
+ }
82
+
83
+ async function ensureBuild(env) {
84
+ const hasServerBuild = existsSync(resolve(projectRoot, "dist-server", "index.mjs"));
85
+ const hasClientBuild = existsSync(resolve(projectRoot, "dist", "client", "index.html"));
86
+
87
+ if (hasServerBuild && hasClientBuild) return;
88
+
89
+ const canBuildFromSource =
90
+ existsSync(resolve(projectRoot, "server")) &&
91
+ existsSync(resolve(projectRoot, "client")) &&
92
+ existsSync(resolve(projectRoot, "package.json"));
93
+
94
+ if (!canBuildFromSource) {
95
+ throw new Error("Bundled build output is missing from this package.");
96
+ }
97
+
98
+ console.log(`[${cliName}] build output not found, running npm run build...`);
99
+ await run(npmCmd, ["run", "build"], env);
100
+ }
101
+
102
+ async function main() {
103
+ const { command, port } = parseArgs(process.argv.slice(2));
104
+
105
+ if (command === "help") {
106
+ printHelp();
107
+ return;
108
+ }
109
+
110
+ const env = port ? { PORT: port } : {};
111
+
112
+ if (command === "build") {
113
+ await run(npmCmd, ["run", "build"], env);
114
+ return;
115
+ }
116
+
117
+ await ensureBuild(env);
118
+ await run(process.execPath, ["dist-server/index.mjs"], {
119
+ ...env,
120
+ NODE_ENV: "production"
121
+ });
122
+ }
123
+
124
+ main().catch((error) => {
125
+ console.error(`[${cliName}] ${error instanceof Error ? error.message : String(error)}`);
126
+ process.exit(1);
127
+ });
@@ -0,0 +1,26 @@
1
+ {
2
+ "favicon.svg": {
3
+ "file": "assets/favicon-_Gh0MYzg.svg",
4
+ "src": "favicon.svg"
5
+ },
6
+ "icon.svg": {
7
+ "file": "assets/icon-BBRFWKI9.svg",
8
+ "src": "icon.svg"
9
+ },
10
+ "index.html": {
11
+ "file": "assets/index-qtlLehV3.js",
12
+ "name": "index",
13
+ "src": "index.html",
14
+ "isEntry": true,
15
+ "dynamicImports": [
16
+ "index.html"
17
+ ],
18
+ "css": [
19
+ "assets/index-D24SA7pm.css"
20
+ ],
21
+ "assets": [
22
+ "assets/favicon-_Gh0MYzg.svg",
23
+ "assets/icon-BBRFWKI9.svg"
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
2
+ <rect x="0" y="0" width="32" height="32" rx="6" fill="#0075de"/>
3
+ <g transform="translate(16, 10)">
4
+ <line x1="-9" y1="0" x2="9" y2="0" stroke="#ffffff" stroke-width="3.5" stroke-linecap="round"/>
5
+ <line x1="-6" y1="0" x2="-6" y2="11" stroke="#ffffff" stroke-width="3.5" stroke-linecap="round"/>
6
+ <line x1="6" y1="0" x2="6" y2="11" stroke="#ffffff" stroke-width="3.5" stroke-linecap="round"/>
7
+ </g>
8
+ </svg>
@@ -0,0 +1,30 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="128" y2="128" gradientUnits="userSpaceOnUse">
4
+ <stop offset="0%" stop-color="#ffffff"/>
5
+ <stop offset="100%" stop-color="#f0f0f0"/>
6
+ </linearGradient>
7
+ <linearGradient id="pi" x1="0" y1="0" x2="128" y2="128" gradientUnits="userSpaceOnUse">
8
+ <stop offset="0%" stop-color="#0075de"/>
9
+ <stop offset="100%" stop-color="#005bab"/>
10
+ </linearGradient>
11
+ </defs>
12
+ <!-- Square background pill-like shape -->
13
+ <rect x="6" y="6" width="116" height="116" rx="24" fill="url(#bg)" stroke="#e0e0e0" stroke-width="2"/>
14
+ <!-- Right angle bracket / message pointer bottom-left -->
15
+ <path d="M30 92 L18 80 L30 80" fill="#eaeaea" stroke="#d6d6d6" stroke-width="1.5" stroke-linejoin="round"/>
16
+ <!-- π symbol -->
17
+ <g transform="translate(64, 42)">
18
+ <!-- Horizontal bar of π -->
19
+ <line x1="-32" y1="0" x2="32" y2="0" stroke="url(#pi)" stroke-width="10" stroke-linecap="round"/>
20
+ <!-- Left leg of π -->
21
+ <line x1="-20" y1="0" x2="-20" y2="36" stroke="url(#pi)" stroke-width="10" stroke-linecap="round"/>
22
+ <!-- Right leg of π -->
23
+ <line x1="20" y1="0" x2="20" y2="36" stroke="url(#pi)" stroke-width="10" stroke-linecap="round"/>
24
+ </g>
25
+ <!-- Small dots / particles suggesting conversation or AI -->
26
+ <circle cx="36" cy="82" r="3.5" fill="#a39e98" opacity="0.5"/>
27
+ <circle cx="50" cy="90" r="2.5" fill="#a39e98" opacity="0.35"/>
28
+ <circle cx="92" cy="82" r="3.5" fill="#a39e98" opacity="0.5"/>
29
+ <circle cx="78" cy="90" r="2.5" fill="#a39e98" opacity="0.35"/>
30
+ </svg>
@@ -0,0 +1 @@
1
+ :root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--primary:#0075de;--primary-active:#005bab;--canvas:#fff;--canvas-soft:#f6f5f4;--hairline:#e6e6e6;--ink:#000;--ink-secondary:#31302e;--ink-muted:#615d59;--ink-faint:#a39e98;--accent-teal:#2a9d99;--sidebar-bg:#f0f0f0;--sidebar-text:#1a1a1a;--sidebar-text-muted:#6e6e6e;--sidebar-hover:#0000000a;--sidebar-active:#0075de14;--sidebar-border:#00000014;--card-tint-mint:#d9f3e1;--card-tint-sky:#dcecfa;--card-tint-yellow:#fef7d6;--semantic-error:#e03131;background:var(--canvas);color:var(--ink);font-synthesis:none;text-rendering:optimizelegibility;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}*{box-sizing:border-box}body{min-width:320px;min-height:100vh;margin:0}button,select,textarea{font:inherit}.app-shell{background:var(--canvas);grid-template-columns:372px minmax(0,1fr);min-height:100vh;transition:grid-template-columns .2s;display:grid}.app-shell-collapsed{grid-template-columns:0 1fr}.sidebar{border-right:1px solid var(--sidebar-border);background:var(--sidebar-bg);height:100vh;color:var(--sidebar-text);flex-direction:column;gap:12px;padding:16px;display:flex;position:relative;overflow:hidden}.sidebar>*{position:relative}.eyebrow{color:var(--ink-faint);letter-spacing:1px;text-transform:uppercase;margin:0 0 8px;font-size:11px;font-weight:600}.sidebar .eyebrow{color:var(--sidebar-text-muted)}h1,h2,h3,p{margin-top:0}h1{max-width:280px;margin-bottom:12px;font-size:42px;font-weight:600;line-height:1.1}h2{color:var(--ink-secondary);margin-bottom:0;font-size:28px;font-weight:600;line-height:1.25}.sidebar-copy{max-width:300px;color:var(--sidebar-text-muted);font-size:16px;line-height:1.55}.brand-block{flex:none}.field{color:var(--ink-secondary);gap:8px;font-size:14px;font-weight:500;display:grid}.sidebar .field{color:var(--sidebar-text)}.field select,.field textarea{border:1px solid var(--hairline);background:var(--canvas);width:100%;color:var(--ink);border-radius:8px;outline:none}.sidebar .field select,.sidebar .field textarea{border-color:var(--sidebar-border);color:var(--sidebar-text);background:#ffffff80}.sidebar .field select option{color:var(--ink)}.field select{min-height:42px;padding:0 12px}.field textarea{resize:vertical;padding:12px;line-height:1.5}.field select:focus,.field textarea:focus{border-color:var(--primary);box-shadow:0 0 0 3px #5645d429}.secondary-button{cursor:pointer;border:0;border:1px solid var(--sidebar-border);min-height:42px;color:var(--sidebar-text);background:0 0;border-radius:8px;font-size:14px;font-weight:500}.secondary-button:hover{background:var(--sidebar-hover)}.sidebar-brand-icon{flex-shrink:0;justify-content:center;align-items:center;margin-right:auto;display:flex}.sidebar-actions{gap:8px;margin-left:auto;display:flex}.sidebar-top-row{align-items:center;gap:8px;display:flex}.sidebar-collapse-btn{border:1px solid var(--sidebar-border);width:28px;height:28px;color:var(--sidebar-text-muted);cursor:pointer;background:0 0;border-radius:6px;justify-content:center;align-self:center;align-items:center;margin-top:auto;transition:background .12s,color .12s;display:flex}.sidebar-collapse-btn:hover{background:var(--sidebar-hover);color:var(--sidebar-text)}.sidebar-expand-btn{border:1px solid var(--hairline);background:var(--canvas);width:24px;height:48px;color:var(--ink-muted);cursor:pointer;z-index:10;border-right:none;border-radius:0 6px 6px 0;justify-content:center;align-items:center;transition:background .12s,color .12s;display:flex;position:fixed;top:50%;left:0;transform:translateY(-50%)}.sidebar-expand-btn:hover{background:var(--canvas-soft);color:var(--primary)}.icon-button{border:1px solid var(--sidebar-border);width:36px;height:36px;color:var(--sidebar-text-muted);cursor:pointer;background:0 0;border-radius:8px;justify-content:center;align-items:center;transition:background .12s,color .12s;display:flex}.icon-button:hover{background:var(--sidebar-hover);color:var(--sidebar-text)}.icon-button:disabled{cursor:not-allowed;opacity:.4}.secondary-button:disabled{cursor:not-allowed;opacity:.55}.session-manager{gap:14px;min-height:0;display:grid}.session-section-heading{color:var(--sidebar-text-muted);text-transform:uppercase;justify-content:space-between;align-items:center;font-size:12px;font-weight:700;display:flex}.session-section-heading small{color:var(--sidebar-text-muted);font-size:12px}.session-list,.archived-sessions{gap:8px;display:grid}.session-list{max-height:min(36vh,360px);padding-right:4px;overflow-y:auto}.session-row{border:1px solid var(--sidebar-border);background:var(--canvas);border-radius:8px;gap:8px;padding:10px;display:grid}.session-row-active{border-color:var(--primary);background:var(--sidebar-active)}.session-row-archived{opacity:.7}.session-select{min-width:0;color:var(--sidebar-text);cursor:pointer;text-align:left;background:0 0;border:0;padding:0;display:grid}.session-select:disabled{cursor:not-allowed}.session-select span{text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:650;overflow:hidden}.session-select small{color:var(--sidebar-text-muted);font-size:12px}.session-row-actions{gap:8px;display:flex}.session-row-actions button{border:1px solid var(--sidebar-border);min-height:28px;color:var(--sidebar-text-muted);cursor:pointer;background:0 0;border-radius:8px;padding:0 9px;font-size:12px;font-weight:600}.session-row-actions button:disabled{cursor:not-allowed;opacity:.5}.session-row-actions button:not(:disabled):hover{background:var(--sidebar-hover);color:var(--sidebar-text)}.session-rename-input{border:1px solid var(--sidebar-border);background:var(--canvas);width:100%;min-height:34px;color:var(--sidebar-text);border-radius:8px;outline:none;padding:0 10px}.pi-sessions-section{flex-direction:column;flex:1;gap:8px;min-height:0;display:flex}.pi-sessions-list{flex:1;align-content:start;gap:4px;min-height:0;padding-right:4px;display:grid;overflow-y:auto}.pi-project-group{gap:2px;display:grid}.pi-project-header-row{align-items:center;gap:4px;display:flex}.pi-project-header{border:1px solid var(--sidebar-border);background:var(--canvas);min-width:0;min-height:32px;color:var(--sidebar-text);cursor:pointer;letter-spacing:-.125px;text-align:left;border-radius:8px;flex:1;align-items:center;gap:8px;padding:6px 10px;font-size:14px;font-weight:600;transition:background .12s;display:flex}.pi-project-header:disabled{cursor:not-allowed;opacity:.45}.pi-project-header:hover:not(:disabled){background:var(--sidebar-hover)}.pi-new-session-btn{border:1px solid var(--sidebar-border);width:32px;height:32px;color:var(--sidebar-text-muted);cursor:pointer;background:0 0;border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;transition:background .12s,color .12s,border-color .12s;display:flex}.pi-new-session-btn:hover:not(:disabled){background:var(--sidebar-hover);border-color:var(--sidebar-border);color:var(--sidebar-text)}.pi-new-session-btn:disabled{cursor:not-allowed;opacity:.45}.pi-project-name{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.pi-project-count{background:var(--sidebar-hover);min-width:24px;color:var(--sidebar-text-muted);letter-spacing:.125px;text-align:center;border-radius:9999px;flex-shrink:0;padding:0 8px;font-size:12px;font-weight:600;line-height:22px}.pi-session-list{gap:2px;padding-left:12px;display:grid}.pi-session-row{cursor:pointer;text-align:left;background:0 0;border:0;border-radius:5px;width:100%;padding:8px 10px;transition:background .12s;display:grid}.pi-session-row:hover{background:var(--sidebar-hover)}.pi-session-row:disabled{cursor:not-allowed;opacity:.46}.pi-session-row-active{background:var(--sidebar-active);border-left:2px solid var(--primary)}.pi-project-group-drag-over{background:var(--sidebar-active);outline:1px dashed var(--primary);outline-offset:-1px;border-radius:8px}.pi-session-info{gap:3px;min-width:0;display:grid}.pi-session-first-msg{color:var(--sidebar-text);text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:400;line-height:1.45;overflow:hidden}.pi-session-meta{color:var(--sidebar-text-muted);letter-spacing:0;align-items:center;gap:6px;font-size:12px;font-weight:400;display:flex}.pi-sessions-loading,.pi-sessions-error,.pi-sessions-empty{color:var(--sidebar-text-muted);padding:8px 10px;font-size:14px}.status-panel{border:1px solid var(--sidebar-border);background:var(--canvas);color:var(--sidebar-text-muted);border-radius:12px;align-items:flex-start;gap:12px;margin-top:auto;padding:16px;line-height:1.5;display:flex}.status-panel p{margin:0}.status-dot{background:var(--accent-teal);border-radius:999px;width:10px;height:10px;margin-top:7px;box-shadow:0 0 0 4px #2a9d992e}.chat-panel{grid-template-rows:auto 1fr auto;min-width:0;max-height:100vh;display:grid}.chat-header{border-bottom:1px solid var(--hairline);background:var(--canvas);justify-content:space-between;align-items:center;gap:16px;padding:10px 32px;display:flex}.chat-header-copy{gap:3px;min-width:0;display:grid}.chat-header-title{color:var(--ink-secondary);text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:600;line-height:1.4;overflow:hidden}.chat-header-meta{color:var(--ink-faint);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:1.4;overflow:hidden}.chat-mode-pill{background:var(--card-tint-sky);min-height:28px;color:var(--ink-secondary);text-transform:uppercase;border-radius:999px;justify-content:center;align-items:center;padding:0 10px;font-size:12px;font-weight:700;display:inline-flex}.messages{background:var(--canvas);min-height:0;overflow:hidden}.messages-empty{flex-direction:column;gap:16px;padding:32px;display:flex;overflow-y:auto}.empty-state{border:1px dashed var(--hairline);background:var(--canvas);max-width:560px;color:var(--ink-muted);text-align:center;border-radius:12px;margin:auto;padding:32px}.empty-state h3{color:var(--ink-secondary)}.chat-bubble-list{height:100%}.chat-bubble-list .ant-bubble-list-scroll-box{height:100%;padding:32px;overflow-y:auto}.chat-bubble-list .ant-bubble-list-scroll-content{flex-direction:column;gap:16px;display:flex}.chat-bubble{max-width:760px;line-height:1.6}.chat-bubble .ant-bubble-content{border:1px solid var(--hairline);background:var(--canvas);white-space:pre-wrap;border-radius:12px;padding:16px 18px}.chat-bubble-user{margin-left:auto}.chat-bubble-user .ant-bubble-content{border-color:var(--card-tint-sky);background:var(--card-tint-sky)}.message-content{gap:12px;display:grid}.message-content p{margin:0}.chat-bubble .ant-bubble-content h1,.chat-bubble .ant-bubble-content h2,.chat-bubble .ant-bubble-content h3,.chat-bubble .ant-bubble-content h4{color:var(--ink-secondary);margin:1em 0 .5em;font-weight:600;line-height:1.3}.chat-bubble .ant-bubble-content h1{font-size:1.4em}.chat-bubble .ant-bubble-content h2{font-size:1.2em}.chat-bubble .ant-bubble-content h3{font-size:1.1em}.chat-bubble .ant-bubble-content p{margin:0 0 .75em;line-height:1.65}.chat-bubble .ant-bubble-content p:last-child{margin-bottom:0}.chat-bubble .ant-bubble-content ul,.chat-bubble .ant-bubble-content ol{margin:0 0 .75em;padding-left:1.5em;line-height:1.65}.chat-bubble .ant-bubble-content li{margin-bottom:.25em}.chat-bubble .ant-bubble-content li:last-child{margin-bottom:0}.chat-bubble .ant-bubble-content blockquote{border-left:3px solid var(--hairline);color:var(--ink-muted);margin:0 0 .75em;padding:.25em 1em}.chat-bubble .ant-bubble-content hr{border:none;border-top:1px solid var(--hairline);margin:1em 0}.chat-bubble .ant-bubble-content a{color:var(--primary);text-underline-offset:2px;text-decoration:underline}.chat-bubble .ant-bubble-content a:hover{color:var(--primary-active)}.chat-bubble .ant-bubble-content strong{font-weight:600}.chat-bubble .ant-bubble-content table{border-collapse:collapse;width:100%;margin:0 0 .75em;font-size:14px}.chat-bubble .ant-bubble-content th,.chat-bubble .ant-bubble-content td{border:1px solid var(--hairline);text-align:left;padding:8px 12px;line-height:1.5}.chat-bubble .ant-bubble-content th{background:var(--canvas-soft);font-weight:600}.message-images{flex-wrap:wrap;gap:10px;display:flex}.message-image{gap:6px;width:min(240px,100%);margin:0;display:grid}.message-image img{border:1px solid var(--hairline);object-fit:cover;border-radius:8px;width:100%;max-height:220px;display:block}.message-image figcaption{color:var(--ink-muted);text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:500;overflow:hidden}.message-meta{color:var(--ink-faint);text-transform:uppercase;justify-content:space-between;align-items:center;gap:16px;margin-bottom:10px;font-size:12px;font-weight:600;display:flex}.message-meta small{color:var(--ink-faint);text-transform:none;font-size:11px;line-height:1.4}.error-banner{max-width:760px;color:var(--semantic-error);background:#fff0ed;border:1px solid #e0313142;border-radius:8px;padding:14px 16px;font-weight:600}.error-banner p{margin:0 0 10px}.inline-action-button{min-height:32px;color:inherit;cursor:pointer;background:0 0;border:1px solid;border-radius:8px;padding:0 12px;font-size:13px;font-weight:600}.inline-action-button:hover{background:#e0313114}.pi-tool-card{border:1px solid var(--hairline);background:var(--canvas-soft);border-radius:10px;overflow:hidden}.pi-tool-card summary{cursor:pointer;color:var(--ink-secondary);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;font-size:14px;font-weight:600;list-style:none;display:flex}.pi-tool-card summary::-webkit-details-marker{display:none}.pi-tool-card summary small{color:var(--ink-faint);font-size:12px;font-weight:600}.pi-tool-card pre{color:var(--ink-secondary);white-space:pre-wrap;word-break:break-word;margin:0;padding:0 14px 14px;font-size:12px;line-height:1.55;overflow-x:auto}.pi-tool-card-error{background:#fff4f2;border-color:#e0313142}.pi-summary-card{border:1px solid var(--hairline);background:var(--canvas-soft);border-radius:10px;gap:8px;padding:14px;display:grid}.pi-summary-card strong{color:var(--ink-secondary);letter-spacing:.02em;text-transform:uppercase;font-size:13px;font-weight:700}.pi-summary-card p{color:var(--ink-muted);margin:0;font-size:14px;line-height:1.6}.pi-summary-card-compaction{background:var(--card-tint-sky)}.pi-summary-card-branch{background:var(--card-tint-yellow)}.pi-summary-card-custom{background:var(--card-tint-mint)}.composer{border-top:1px solid var(--hairline);background:var(--canvas);padding:12px 20px}.pi-history-footer{border-top:1px solid var(--hairline);background:var(--canvas-soft);color:var(--ink-faint);align-items:center;padding:18px 32px 24px;font-size:13px;font-weight:600;display:flex}.attachment-preview{border:1px solid var(--hairline);background:var(--canvas);border-radius:8px;grid-template-columns:56px minmax(0,1fr) auto;align-items:center;gap:12px;margin-bottom:12px;padding:10px;display:grid}.attachment-preview img{object-fit:cover;border-radius:6px;width:56px;height:56px}.attachment-preview div{gap:2px;min-width:0;display:grid}.attachment-preview strong,.attachment-preview span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.attachment-preview strong{color:var(--ink);font-size:14px;font-weight:600}.attachment-preview span{color:var(--ink-faint);font-size:12px}.attachment-preview button{border:1px solid var(--hairline);background:var(--canvas);min-height:32px;color:var(--ink-secondary);cursor:pointer;border-radius:8px;padding:0 12px;font-size:13px;font-weight:600}.attachment-preview button:hover,.attachment-preview button:not(:disabled):hover{border-color:var(--primary);color:var(--primary)}.composer-upload-input{display:none}.chat-sender{border:1px solid var(--hairline);background:var(--canvas-soft);border-radius:8px}.chat-sender .ant-sender-content{grid-template-columns:minmax(0,1fr) auto;align-items:center;column-gap:10px;min-height:64px;padding:10px;display:grid}.chat-sender .ant-sender-actions-list{place-self:end;margin:0;padding:0 0 2px}.chat-sender .ant-sender-input{min-width:0;font-size:14px;line-height:1.5}.chat-sender .ant-sender-actions-btn .ant-btn-primary{background:var(--primary)}.chat-sender .ant-sender-actions-btn .ant-btn-primary:not(:disabled):hover{background:var(--primary-active)}.slash-command-suggestion{width:100%}.slash-command-option{gap:2px;min-width:240px;display:grid}.slash-command-option span{color:var(--ink);font-size:14px;font-weight:600}.slash-command-option small{color:var(--ink-faint);font-size:12px;line-height:1.35}.slash-command-source{color:var(--ink-faint);text-transform:uppercase;font-size:11px;font-weight:600}.settings-modal-content{gap:18px;padding-top:4px;display:grid}@media (width<=860px){.app-shell{grid-template-columns:1fr}.sidebar{border-right:0;border-bottom:1px solid var(--sidebar-border)}.session-list{max-height:280px}.chat-panel{max-height:none}}@media (width<=560px){.sidebar,.messages-empty,.chat-bubble-list .ant-bubble-list-scroll-box,.composer{padding-left:18px;padding-right:18px}.attachment-preview{grid-template-columns:48px minmax(0,1fr)}.attachment-preview button{grid-column:1/-1;width:100%}.chat-header{padding:8px 18px}}