@tianshu-ai/tianshu 0.3.19 → 0.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tianshu-ai/tianshu",
3
- "version": "0.3.19",
3
+ "version": "0.3.20",
4
4
  "description": "An open AI agent platform with a sidecar browser. Built in public.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/tianshu-ai/tianshu",
@@ -56,7 +56,8 @@
56
56
  { "id": "task_retry_fresh", "module": "TaskRetryFreshTool" },
57
57
  { "id": "task_extend_timeout", "module": "TaskExtendTimeoutTool" },
58
58
  { "id": "task_abort", "module": "TaskAbortTool" },
59
- { "id": "model_list", "module": "ModelListTool" }
59
+ { "id": "model_list", "module": "ModelListTool" },
60
+ { "id": "worker_analytics", "module": "WorkerAnalyticsTool" }
60
61
  ],
61
62
  "skills": [
62
63
  { "id": "workboard-howto", "path": "skills/workboard-howto.md" },
@@ -22,7 +22,7 @@ Personal directories (use freely):
22
22
  - Don't leave scratch artefacts in ./projects/ or the root — use ./tmp/.
23
23
  - Other users' homes in this tenant are off-limits; you cannot reach them.`},{id:`workspace-references`,text:`Referencing files in your reply:
24
24
  - When you point at a workspace file in your response (e.g. an image you just wrote, a generated report, an attachment), use the workspace:// URI you got back from the fs tool, e.g. \`![cover](workspace:///projects/x/cover.png)\`.
25
- - The chat UI knows how to render those; absolute host paths or bare "./foo" links won't preview.`},{id:`edit-vs-write`,text:"- `edit_file` is for changes to an existing file (in-place exact-text replace, atomic). `write_file` is for new files or full rewrites of the entire file. Prefer editing existing files; do not create new files unless asked."},{id:`edit-old-text-uniqueness`,text:'- Each `edit_file` `old_text` must appear exactly once in the file. Keep `old_text` as small as possible while still being unique — don\'t pad with large unchanged regions to "connect" two distant changes; emit two separate edits in the same `edits[]` batch instead.'},{id:`read-before-edit`,text:"- Before `edit_file` (or `write_file` overwrite) on a file you haven't seen yet in this session, call `read_file` once. The host tracks reads per session, so a single full-file read covers any number of follow-up `edit_file` / `write_file` calls on that same path — don't re-read between edits. Files you just created with `write_file` count as already-read; you can `edit_file` them right away without reading first."}]}},rF={id:`microsandbox`,version:`0.1.0`,displayName:`MicroSandbox`,description:`Per-tenant shell sandbox running on microsandbox (Apple Virtualization.framework / KVM). Provides sandbox.shell + an opt-in browser.cdp sidecar (Stealth Chromium via CloakBrowser + Playwright MCP + noVNC; lit up by a follow-up PR).`,author:`tianshu-ai`,license:`Apache-2.0`,permissions:[`workspace.read`,`workspace.write`,`process.spawn`],provides:[`sandbox.shell`,`sandbox.taskPool`,`browser.cdp`],client:{entry:`@tianshu-builtin/plugin-microsandbox/client`},server:{entry:`@tianshu-builtin/plugin-microsandbox/server`},contributes:{topBarButtons:[{id:`browser`,icon:`Globe`,tooltip:`Browser viewport (noVNC)`,opensPanel:`microsandbox.browser`,order:210}],rightPanels:[{id:`browser`,displayName:`Browser`,component:`BrowserViewportPanel`}],adminPages:[{id:`main`,displayName:`Sandbox`,icon:`Box`,component:`MicroSandboxAdminPage`,group:`Plugins`,order:100}],sandboxes:[{id:`main`,kind:`shell`,displayName:`MicroSandbox`,module:`MicroSandboxRunner`}],tools:[{id:`exec`,module:`ExecTool`},{id:`reset_sandbox`,module:`ResetSandboxTool`},{id:`get_sandbox_status`,module:`GetSandboxStatusTool`},{id:`update_sandbox_config`,module:`UpdateSandboxConfigTool`},{id:`build_sandbox`,module:`BuildSandboxTool`},{id:`list_sandbox_builds`,module:`ListSandboxBuildsTool`},{id:`use_sandbox_build`,module:`UseSandboxBuildTool`},{id:`browser_health_check`,module:`BrowserHealthCheckTool`}],toolsets:[{id:`browser`,module:`BrowserToolset`,displayName:`Playwright (Browser)`}],skills:[{id:`exec-howto`,path:`skills/microsandbox-exec-howto.md`},{id:`config`,path:`skills/microsandbox-config.md`},{id:`build-use`,path:`skills/microsandbox-build-use.md`},{id:`browser-howto`,path:`skills/microsandbox-browser-howto.md`},{id:`libreoffice`,path:`skills/microsandbox-libreoffice.md`}],systemPromptFragments:[{id:`no-foreground-servers`,text:"- Long-running servers (`python -m http.server`, `npm start`, `npm run dev`, anything that listens on a port and never returns) WILL hang the `exec` call until the host's per-call timeout fires (5 min default). Even with `nohup` alone the call hangs because microsandbox's exec channel waits for the started process tree's stdio to close, and nohup doesn't close stdin or detach from the controlling terminal.\n\n If you need to start a server, the ONLY safe pattern is:\n\n nohup setsid <cmd> > /tmp/srv.log 2>&1 < /dev/null &\n\n All four pieces are required: `setsid` puts the process in a new session (no controlling terminal); `< /dev/null` closes stdin; `>/tmp/srv.log 2>&1` redirects stdout AND stderr; trailing `&` puts it in the shell background. Skip any one and exec hangs.\n\n Then verify with `curl http://localhost:<port>/...` in a separate exec call. Don't background-then-curl in the same shell line — the shell process itself becomes the parent and won't exit."},{id:`already-installed-runtimes`,text:"- The sandbox already ships chromium (CDP 9222), Playwright MCP (3200), LibreOffice, Node 22, and Python 3.12. Don't `apt install` / `pip install` / `playwright install` what's already there."},{id:`task-sandbox-is-ephemeral`,text:"- If you are running as a workboard task worker (you have a workboard task id and your run will end with `task_complete`), the sandbox you `exec` in is per-task and gets stopped the moment the task terminates. Anything you nohup / setsid / disown into the background dies with the sandbox — don't try to leave servers / queues / cron jobs / file watchers running for someone else to consume. Run the work inline, capture its output to a file under your home, then `task_complete` with the file path.\n\n Concrete VERIFY-task pattern: if the deliverable is an HTML / PDF / image file and you want to confirm it renders, DON'T spin up `python -m http.server` to serve it. Either (a) use a Playwright MCP `browser_navigate` to `file:///workspace/users/<self>/projects/<slug>/report.html` directly (Playwright MCP is configured with `--allow-unrestricted-file-access`, so file:// URLs work), or (b) `cat` / `head` the file and inspect the structure inline, or (c) for charts, check the rendered file's size + that it contains expected canvas/svg/img markup (`grep -c '<canvas\\|<svg\\|<img' report.html`). Spinning up a transient http server adds a process that has to be cleanly torn down before task_complete, and the typical agent forgets the `setsid + </dev/null` pattern and hangs the run.\n\n Long-lived background services belong in a chat session (the long-lived sandbox there survives between turns), not in a task. Installed packages, files you wrote, and other on-disk state DO survive across attempts of the same task — only running processes don't."}],apiRoutes:[{method:`GET`,path:`/status`,handler:`status`},{method:`GET`,path:`/sandboxfile`,handler:`getSandboxfile`},{method:`PUT`,path:`/sandboxfile`,handler:`putSandboxfile`},{method:`GET`,path:`/sandboxfile/templates`,handler:`getSandboxfileTemplates`},{method:`GET`,path:`/builds`,handler:`getBuilds`},{method:`POST`,path:`/builds`,handler:`postBuilds`},{method:`POST`,path:`/builds/use`,handler:`postUseBuild`},{method:`POST`,path:`/reset`,handler:`postReset`},{method:`POST`,path:`/exec`,handler:`postExec`},{method:`GET`,path:`/task-pool`,handler:`getTaskPool`},{method:`POST`,path:`/task-pool/destroy`,handler:`postTaskPoolDestroy`},{method:`GET`,path:`/browser/status`,handler:`getBrowserStatus`},{method:`POST`,path:`/browser/restart`,handler:`postBrowserRestart`},{method:`POST`,path:`/browser/resize`,handler:`postBrowserResize`}]},configSchema:{fields:[{key:`memoryMib`,label:`Browser sandbox memory`,kind:`number`,unit:`MiB`,min:512,max:65536,step:512,default:2048,description:`RAM allotted to the long-lived Browser sandbox VM. Increase to 8192+ MiB for browser-heavy workloads (chromium + Playwright MCP), 12288+ for build-time pip / npm of large stacks. Restart the sandbox (Reset) for the new value to take effect.`},{key:`cpus`,label:`Browser sandbox vCPU count`,kind:`number`,min:1,max:16,step:1,default:2,description:`Logical CPU count handed to the long-lived Browser sandbox VM. Reset the sandbox after changing.`},{key:`execTimeoutMs`,label:`Per-exec timeout (Browser)`,kind:`number`,unit:`ms`,min:5e3,max:18e5,step:5e3,default:3e5,description:"Max wall-clock time for one Browser `exec` call before it's force-killed. Bump this for long-running installers; consider running them with `nohup setsid … &` instead."},{key:`taskMemoryMib`,label:`Task sandbox memory`,kind:`number`,unit:`MiB`,min:512,max:65536,step:512,default:2048,description:`RAM allotted to per-task sandbox VMs (forked from the Task snapshot). Set lower than the Browser value to fit more parallel tasks; raise it for tasks that need a heavy data-science stack. Effective on the next task acquire.`},{key:`taskCpus`,label:`Task sandbox vCPU count`,kind:`number`,min:1,max:16,step:1,default:2,description:`Logical CPU count handed to each per-task sandbox VM. Effective on the next task acquire.`},{key:`taskExecTimeoutMs`,label:`Per-exec timeout (Task)`,kind:`number`,unit:`ms`,min:5e3,max:18e5,step:5e3,default:3e5,description:"Max wall-clock time for one Task `exec` call before it's force-killed. Per-task sandboxes typically run shorter, more focused commands than the Browser sandbox."},{key:`taskIdleShutdownMs`,label:`Task sandbox idle shutdown`,kind:`number`,unit:`ms`,min:0,max:864e5,step:6e4,default:6e5,description:`How long a task sandbox stays alive after its task ends before it's stopped. 0 = stop immediately on release. Higher values trade RAM for faster reuse when the same kind of task runs again soon.`}]}},iF={id:`web-search`,version:`0.1.0`,displayName:`Web Search`,description:`Web search via Tavily and Brave Search APIs. One tool, two providers, automatic fallback. Set API keys in Settings → Plugins.`,author:`tianshu-ai`,license:`Apache-2.0`,permissions:[`network.external`],server:{entry:`@tianshu-builtin/plugin-web-search/server`},contributes:{tools:[{id:`web_search`,module:`WebSearchTool`}],skills:[{id:`web-search-howto`,path:`skills/web-search-howto.md`}],apiRoutes:[{method:`GET`,path:`/health`,handler:`getHealth`},{method:`POST`,path:`/health/reset`,handler:`resetHealth`}]},configSchema:{fields:[{key:`tavilyApiKey`,label:`Tavily API key`,kind:`secret`,placeholder:`tvly-...`,description:`Sign up at tavily.com (free tier 1000 searches/month). Stored under <tenant>/secrets/, never returned to the browser.`},{key:`braveApiKey`,label:`Brave Search API key`,kind:`secret`,placeholder:`BSA...`,description:`Optional. Get a free key at api-dashboard.search.brave.com (2000/month). Used as fallback when Tavily fails or returns nothing.`},{key:`preferredProvider`,label:`Preferred provider`,kind:`select`,default:`tavily`,options:[{label:`Tavily`,value:`tavily`},{label:`Brave`,value:`brave`}],description:`Tried first when both keys are set. The other provider is used as automatic fallback only on transient errors (network / 5xx / 429). Auth failures (401 / 403) park the bad key in the health cache; clear it from Settings after fixing the key.`},{key:`timeoutMs`,label:`Per-request timeout`,kind:`number`,unit:`ms`,min:1e3,max:6e4,step:500,default:8e3,description:`Network timeout for each provider call. Increase if you hit timeouts on slow networks.`}]}},aF={id:`workboard`,version:`0.1.0`,displayName:`Workboard`,description:`Kanban task board with a worker pool. The orchestrator drops tasks onto the board; workers pick them up, run them, and report back. v0.2 ships an echo worker so the full ready→in-progress→done loop is visible end-to-end; real worker runtimes (qianliyan / luban / xihe / nvwa) land in follow-up PRs as separate plugins.`,author:`tianshu-ai`,license:`Apache-2.0`,permissions:[`workspace.read`,`tenant.db`],requires:[`host.agentLoop`,`host.sessionInbox`,`host.toolCatalog`,`host.skillCatalog`,`host.modelCatalog`],client:{entry:`@tianshu-builtin/plugin-workboard/client`},server:{entry:`@tianshu-builtin/plugin-workboard/server`},contributes:{topBarButtons:[{id:`toggle`,icon:`Kanban`,tooltip:`Task board`,opensPanel:`workboard.main`,order:200}],rightPanels:[{id:`main`,displayName:`Tasks`,component:`WorkboardPanel`}],sidebarSections:[{id:`workers`,displayName:`Workers`,component:`WorkersSidebarSection`,after:`workers`,order:100}],adminPages:[{id:`agents`,displayName:`Worker agents`,icon:`Bot`,component:`WorkerAgentsPage`,order:10}],tools:[{id:`task_list`,module:`TaskListTool`},{id:`task_create`,module:`TaskCreateTool`},{id:`task_update`,module:`TaskUpdateTool`},{id:`task_move`,module:`TaskMoveTool`},{id:`task_delete`,module:`TaskDeleteTool`},{id:`task_get_history`,module:`TaskGetHistoryTool`},{id:`task_complete`,module:`TaskCompleteTool`},{id:`task_continue`,module:`TaskContinueTool`},{id:`task_retry_fresh`,module:`TaskRetryFreshTool`},{id:`task_extend_timeout`,module:`TaskExtendTimeoutTool`},{id:`task_abort`,module:`TaskAbortTool`},{id:`model_list`,module:`ModelListTool`}],skills:[{id:`workboard-howto`,path:`skills/workboard-howto.md`},{id:`worker-creator`,path:`skills/worker-creator.md`},{id:`worker-fleet`,path:`skills/worker-fleet.md`},{id:`large-input-large-output`,path:`skills/large-input-large-output.md`}],agentSeeds:[{id:`echo-demo`,path:`agent-seeds/echo-demo`},{id:`llm-default`,path:`agent-seeds/llm-default`}],systemPromptFragments:[{id:`prefer-delegation`,text:'The workboard is active in this tenant. For any non-trivial user request — anything that takes more than a few minutes, fans out into multiple steps, or has clearly separable roles (design / build / test, research / write, etc.) — your default is to DELEGATE to worker agents via `task_create` rather than execute it yourself. The `worker-fleet` skill covers task decomposition (plan / design / build / verify phases), task sizing (avoiding max_tokens stalls on monolithic tasks), worker selection (matching by capability), and brief authoring (each task description is the worker\'s only view of the work) — read it before designing the task graph. Reuse existing workers (run `tenant_config_list({path:"workers"})` first); the `worker-creator` skill covers authoring agent.json + SOUL.md when no existing worker fits. Do the work yourself directly only when (a) the user explicitly says so, (b) the request is trivial (single tool call, one-line answer, quick lookup), or (c) the user wants a synchronous reply. When in doubt, ack the plan and give the user a chance to override.'},{id:`summarize-on-completion`,text:"When the workboard tasks you delegated for the user's current request have all finished, your job isn't done — you owe the user a synthesised reply. Steps: (1) Call `task_list({status:\"done\"})` (or filter by labels/ids you tracked) to confirm every task you created in this turn has actually completed; if any are still running, wait or fetch their status before composing. (2) For each completed task, call `task_get_history({id})` and READ the worker's actual output — don't trust `resultSummary` alone; workers sometimes write a one-liner that hides nuance, errors, or important findings. Skim the final assistant message + any tool results that produced files. (3) Compose ONE coherent reply to the user that: covers the original goal, integrates the per-task findings into a narrative (not a bulleted dump of resultSummaries), explicitly cites every output file the workers produced (use the file's path, and where the tenant has the `files` plugin enabled use the standard file-reference markup the user's chat client understands so they can click through), and surfaces any failures / partial results honestly. (4) If a task ended in awaiting-intervention, do NOT just report the failure — either resolve it yourself (task_continue / task_retry_fresh / task_extend_timeout / task_abort) before replying, or include in the reply a concrete suggestion for what the user should choose. Keep the synthesis honest: if outputs contradict each other or a worker punted, say so; don't paper over with cheerful language."}],apiRoutes:[{method:`GET`,path:`/tasks`,handler:`listTasks`},{method:`POST`,path:`/tasks`,handler:`createTask`},{method:`POST`,path:`/tasks/delete`,handler:`deleteTask`},{method:`PATCH`,path:`/tasks/:id`,handler:`patchTask`},{method:`GET`,path:`/tasks/:id/history`,handler:`taskHistory`},{method:`GET`,path:`/projects`,handler:`listProjects`},{method:`GET`,path:`/workers/status`,handler:`workerStatus`},{method:`POST`,path:`/workers/restart`,handler:`workerRestart`},{method:`GET`,path:`/agents`,handler:`listAgents`},{method:`PATCH`,path:`/agents/:slug/enabled`,handler:`setAgentEnabled`}]},configSchema:{fields:[{key:`echo.enabled`,label:`Enabled`,kind:`boolean`,default:!0,group:{id:`worker-type-echo`,label:`echo`,badge:`worker type`}},{key:`echo.delayMs`,label:`Simulated execution delay`,description:`How long an echo worker pretends to be busy on each task before marking it done. 0 makes tasks complete instantly.`,kind:`number`,default:3e4,min:0,step:1e3,unit:`ms`,group:{id:`worker-type-echo`,label:`echo`,badge:`worker type`}},{key:`llm.enabled`,label:`Enabled`,kind:`boolean`,default:!0,group:{id:`worker-type-llm`,label:`llm`,badge:`worker type`}},{key:`llm.firstResponseSec`,label:`First-response timeout`,description:`Hard cap on how long the LLM has to start streaming the very first event of the run. Catches connection-hung scenarios. Set to 0 to disable.`,kind:`number`,default:300,min:0,step:30,unit:`s`,group:{id:`worker-type-llm`,label:`llm`,badge:`worker type`}},{key:`llm.idleSec`,label:`Idle timeout`,description:"Mid-stream silence cap: if the LLM goes this long without emitting any event, abort the run as stuck. Long enough to allow large `write_file` arguments to stream through.",kind:`number`,default:600,min:0,step:60,unit:`s`,group:{id:`worker-type-llm`,label:`llm`,badge:`worker type`}},{key:`llm.maxRunSec`,label:`Max run time`,description:`Total wall-clock cap for one task run, across all turns. Protects against runaway loops.`,kind:`number`,default:1800,min:0,step:60,unit:`s`,group:{id:`worker-type-llm`,label:`llm`,badge:`worker type`}}]}},oF=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),J=o(((e,t)=>{t.exports=oF()}))();function sF(){return globalThis}function cF(e){sF().__tianshuPluginSdkComposer__=e}function lF(){return globalThis}function uF(e){lF().__tianshuPluginSdkOpenFile__=e}function dF(){return globalThis}function fF(e){dF().__tianshuPluginSdkPluginConfigForm__=e}var pF=e=>{let t=dF().__tianshuPluginSdkPluginConfigForm__;return t?S.createElement(t,e):null};function mF(){return e=>{let t=lF().__tianshuPluginSdkOpenFile__;if(!t){let t=e.replace(/^workspace:\/\/+/,`/`),n=`/api/p/files/raw?path=${encodeURIComponent(t)}`;window.open(n,`_blank`,`noopener`);return}t.open(e)}}var hF=`/api/p/files`,gF=new Set([`.png`,`.jpg`,`.jpeg`,`.gif`,`.webp`,`.bmp`,`.ico`,`.svg`]),_F=new Set([`.js`,`.cjs`,`.mjs`,`.ts`,`.jsx`,`.tsx`,`.py`,`.rb`,`.go`,`.rs`,`.c`,`.cpp`,`.h`,`.java`,`.kt`,`.swift`,`.sh`,`.bash`,`.zsh`,`.lua`,`.php`,`.pl`,`.vue`,`.svelte`]),vF=new Set([`.json`,`.yaml`,`.yml`,`.toml`,`.xml`,`.csv`,`.tsv`]),yF=new Set([`.mp4`,`.avi`,`.mov`,`.mkv`,`.webm`,`.m4v`]),bF=new Set([`.mp3`,`.wav`,`.ogg`,`.flac`,`.aac`,`.m4a`]),xF=new Set([`.zip`,`.tar`,`.gz`,`.tgz`,`.rar`,`.7z`,`.bz2`]),SF=new Set([`.md`,`.markdown`,`.txt`,`.log`,`.html`,`.css`,`.env`]);function CF(e){if(e.type===`directory`)return(0,J.jsx)($h,{size:16,className:`text-yellow-500`});let t=(e.extension??``).toLowerCase();return gF.has(t)?(0,J.jsx)(Fv,{size:16,className:`text-green-400`}):_F.has(t)?(0,J.jsx)(Vd,{size:16,className:`text-blue-400`}):vF.has(t)?(0,J.jsx)(Jm,{size:16,className:`text-orange-400`}):yF.has(t)?(0,J.jsx)(uh,{size:16,className:`text-purple-400`}):bF.has(t)?(0,J.jsx)(YS,{size:16,className:`text-pink-400`}):xF.has(t)?(0,J.jsx)(Ci,{size:16,className:`text-gray-400`}):SF.has(t)?(0,J.jsx)(Qm,{size:16,className:`text-gray-300`}):(0,J.jsx)(sh,{size:16,className:`text-gray-500`})}function wF(e){return e===0?`—`:e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MB`:`${(e/(1024*1024*1024)).toFixed(1)} GB`}function TF(e){if(e===0)return`—`;let t=Date.now()-e;return t<6e4?`Just now`:t<36e5?`${Math.floor(t/6e4)}m ago`:t<864e5?`${Math.floor(t/36e5)}h ago`:t<7*864e5?`${Math.floor(t/864e5)}d ago`:new Date(e).toLocaleDateString()}function EF({plugin:e}){let[t,n]=(0,S.useState)(`/`),[r,i]=(0,S.useState)(null),[a,o]=(0,S.useState)(!1),[s,c]=(0,S.useState)(null),[l,u]=(0,S.useState)(``),[d,f]=(0,S.useState)(`name`),[p,m]=(0,S.useState)(`asc`),[h,g]=(0,S.useState)(`list`),[_,v]=(0,S.useState)(null),y=(0,S.useCallback)(async e=>{o(!0),c(null);try{let t=await fetch(`${hF}/list?dir=${encodeURIComponent(e)}`,{credentials:`include`});if(!t.ok)throw Error(`${t.status}`);i(await t.json())}catch(e){c(e instanceof Error?e.message:String(e)),i(null)}finally{o(!1)}},[]);(0,S.useEffect)(()=>{y(t)},[t,y]);let b=(0,S.useMemo)(()=>{if(!r)return[];let e=[...r.entries];return e.sort((e,t)=>{if(e.type!==t.type)return e.type===`directory`?-1:1;let n=0;return n=d===`name`?e.name.localeCompare(t.name):d===`size`?e.size-t.size:e.modifiedMs-t.modifiedMs,p===`asc`?n:-n}),e},[r,d,p]),x=(0,S.useMemo)(()=>{if(!l)return b;let e=l.toLowerCase();return b.filter(t=>t.name.toLowerCase().includes(e))},[b,l]),C=e=>{u(``),n(e)};function w(e){d===e?m(e=>e===`asc`?`desc`:`asc`):(f(e),m(`asc`))}let T=t===`/`?[`/`]:[`/`,...t.split(`/`).filter(Boolean)];return(0,J.jsxs)(`div`,{className:`flex h-full flex-col bg-gray-950 text-gray-300`,children:[(0,J.jsxs)(`div`,{className:`space-y-2 border-b border-gray-800 px-3 py-2`,children:[(0,J.jsxs)(`div`,{className:`flex items-center gap-1 overflow-x-auto text-sm`,children:[t!==`/`&&(0,J.jsx)(`button`,{type:`button`,onClick:()=>C(t.substring(0,t.lastIndexOf(`/`))||`/`),className:`mr-1 shrink-0 text-gray-500 hover:text-gray-300`,title:`Up one level`,children:(0,J.jsx)(Ji,{size:14})}),T.map((e,t)=>{let n=t===0?`/`:`/`+T.slice(1,t+1).join(`/`),r=t===T.length-1;return(0,J.jsxs)(`span`,{className:`flex shrink-0 items-center`,children:[t>0&&(0,J.jsx)(zl,{size:12,className:`mx-0.5 text-gray-600`}),(0,J.jsx)(`button`,{type:`button`,onClick:()=>C(n),className:[`rounded px-1 hover:text-white`,r?`font-medium text-white`:`text-gray-500`].join(` `),children:t===0?`~`:e})]},t)})]}),(0,J.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,J.jsxs)(`div`,{className:`relative flex-1`,children:[(0,J.jsx)(fD,{size:14,className:`pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-500`}),(0,J.jsx)(`input`,{type:`text`,value:l,onChange:e=>u(e.target.value),placeholder:`Filter files...`,className:`w-full rounded border border-gray-800 bg-gray-900 py-1 pl-8 pr-2 text-sm text-gray-300 placeholder-gray-600 focus:border-gray-600 focus:outline-none`})]}),(0,J.jsx)(`button`,{type:`button`,onClick:()=>y(t),className:`rounded p-1.5 text-gray-400 hover:bg-gray-800 hover:text-gray-200`,title:`Refresh`,children:(0,J.jsx)(ZT,{size:14,className:a?`animate-spin`:``})}),(0,J.jsx)(`button`,{type:`button`,onClick:()=>g(h===`list`?`grid`:`list`),className:`rounded p-1.5 text-gray-400 hover:bg-gray-800 hover:text-gray-200`,title:`Toggle view`,children:h===`list`?(0,J.jsx)(Sy,{size:14}):(0,J.jsx)(wy,{size:14})})]})]}),h===`list`&&(0,J.jsxs)(`div`,{className:`flex select-none items-center border-b border-gray-800/50 px-3 py-1 text-xs text-gray-500`,children:[(0,J.jsxs)(`button`,{type:`button`,onClick:()=>w(`name`),className:`flex flex-1 items-center gap-1 text-left hover:text-gray-300`,children:[`Name `,d===`name`&&(0,J.jsx)(na,{size:10})]}),(0,J.jsxs)(`button`,{type:`button`,onClick:()=>w(`size`),className:`flex w-20 items-center justify-end gap-1 text-right hover:text-gray-300`,children:[`Size `,d===`size`&&(0,J.jsx)(na,{size:10})]}),(0,J.jsxs)(`button`,{type:`button`,onClick:()=>w(`modified`),className:`flex w-24 items-center justify-end gap-1 text-right hover:text-gray-300`,children:[`Modified `,d===`modified`&&(0,J.jsx)(na,{size:10})]})]}),(0,J.jsxs)(`div`,{className:`flex-1 overflow-auto`,children:[a&&!r?(0,J.jsx)(`div`,{className:`flex h-32 items-center justify-center`,children:(0,J.jsx)(db,{size:20,className:`animate-spin text-gray-500`})}):s?(0,J.jsx)(`div`,{className:`p-4 text-center text-sm text-rose-400`,children:s}):x.length===0?(0,J.jsx)(`div`,{className:`p-8 text-center text-sm text-gray-600`,children:l?`No matching files`:`Empty directory`}):h===`list`?(0,J.jsx)(`ul`,{className:`divide-y divide-gray-800/30`,children:x.map(e=>(0,J.jsxs)(`li`,{className:`group flex cursor-pointer items-center px-3 py-1.5 hover:bg-gray-800/50`,onClick:()=>{e.type===`directory`?C(e.path):v(e)},children:[(0,J.jsxs)(`div`,{className:`flex min-w-0 flex-1 items-center gap-2`,children:[CF(e),(0,J.jsx)(`span`,{className:`truncate text-sm`,children:e.name})]}),(0,J.jsx)(`span`,{className:`w-20 text-right text-xs text-gray-500`,children:e.type===`file`?wF(e.size):`—`}),(0,J.jsx)(`span`,{className:`w-24 text-right text-xs text-gray-500`,children:TF(e.modifiedMs)})]},e.path))}):(0,J.jsx)(`div`,{className:`grid grid-cols-3 gap-2 p-3`,children:x.map(e=>(0,J.jsxs)(`button`,{type:`button`,onClick:()=>{e.type===`directory`?C(e.path):v(e)},className:`flex cursor-pointer flex-col items-center rounded-lg p-3 hover:bg-gray-800/50`,children:[(0,J.jsx)(`div`,{className:`mb-1`,children:CF(e)}),(0,J.jsx)(`span`,{className:`w-full truncate text-center text-xs`,children:e.name})]},e.path))}),r?.truncated&&(0,J.jsx)(`div`,{className:`px-3 py-2 text-[10px] text-amber-400`,children:`Listing truncated at 5000 entries.`})]}),_&&(0,J.jsx)(DF,{entry:_,onClose:()=>v(null)})]})}function DF({entry:e,onClose:t}){let[n,r]=(0,S.useState)(null),[i,a]=(0,S.useState)(null),o=(e.extension??``).toLowerCase(),s=gF.has(o);(0,S.useEffect)(()=>{if(s){r({path:e.path,size:e.size,modifiedMs:e.modifiedMs,binary:!0});return}let t=!1;return a(null),fetch(`${hF}/read?path=${encodeURIComponent(e.path)}`,{credentials:`include`}).then(async e=>{if(!e.ok){if(e.status===413){let t=await e.json();throw Error(`File is ${wF(t.size)}, exceeds the ${wF(t.maxBytes)} preview cap.`)}throw Error(`HTTP ${e.status}`)}return await e.json()}).then(e=>{t||r(e)}).catch(e=>{t||a(e instanceof Error?e.message:String(e))}),()=>{t=!0}},[e.path,e.size,e.modifiedMs,s]);let c=(0,S.useRef)(null);return(0,S.useEffect)(()=>{let e=e=>{e.key===`Escape`&&t()};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[t]),(0,J.jsx)(`div`,{ref:c,className:`fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-6 backdrop-blur-sm`,onClick:t,children:(0,J.jsxs)(`div`,{className:`flex h-full max-h-[80vh] w-full max-w-3xl flex-col overflow-hidden rounded-xl border border-gray-700 bg-gray-900 shadow-2xl`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`flex items-center justify-between border-b border-gray-800 px-4 py-2.5`,children:[(0,J.jsxs)(`div`,{className:`flex min-w-0 items-center gap-2`,children:[CF(e),(0,J.jsx)(`span`,{className:`truncate text-sm font-medium text-gray-100`,children:e.name}),(0,J.jsxs)(`span`,{className:`flex-shrink-0 text-[11px] text-gray-500`,children:[wF(e.size),` · `,TF(e.modifiedMs)]})]}),(0,J.jsx)(`button`,{type:`button`,onClick:t,className:`btn-ghost p-1.5`,"aria-label":`Close`,children:(0,J.jsx)(IP,{size:16})})]}),(0,J.jsx)(`div`,{className:`flex-1 overflow-auto bg-gray-950`,children:i?(0,J.jsx)(`div`,{className:`p-6 text-center text-sm text-rose-300`,children:i}):n?s?(0,J.jsx)(`img`,{src:`${hF}/raw?path=${encodeURIComponent(e.path)}`,alt:e.name,className:`mx-auto max-h-full max-w-full`}):n.binary?(0,J.jsxs)(`div`,{className:`p-6 text-center text-sm text-gray-500`,children:[`Binary file (`,wF(n.size),`). No preview available.`]}):(0,J.jsx)(`pre`,{className:`whitespace-pre-wrap break-all p-4 font-mono text-xs text-gray-200`,children:n.content}):(0,J.jsx)(`div`,{className:`flex h-32 items-center justify-center`,children:(0,J.jsx)(db,{size:20,className:`animate-spin text-gray-500`})})})]})})}var OF=50*1024*1024;function kF(e){let{composer:t}=e,n=(0,S.useRef)(null);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`button`,{type:`button`,onClick:()=>n.current?.click(),title:`Attach file`,"aria-label":`Attach file`,className:`rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-gray-700 hover:text-gray-200`,children:(0,J.jsx)(nw,{size:16})}),(0,J.jsx)(`input`,{ref:n,type:`file`,multiple:!0,className:`hidden`,onChange:e=>{let n=e.target.files?Array.from(e.target.files):[];e.target.value=``;for(let e of n){if(e.size>OF){t.addAttachment({name:e.name,size:e.size,status:`error`,error:`File exceeds ${(OF/1024/1024).toFixed(0)} MB cap`,mimeType:e.type||`application/octet-stream`});continue}AF(e,t.addAttachment({name:e.name,size:e.size,status:`uploading`,mimeType:e.type||`application/octet-stream`}),t)}}})]})}async function AF(e,t,n){try{let r=await fetch(`${hF}/upload`,{method:`POST`,headers:{"Content-Type":`application/octet-stream`,"X-Filename":encodeURIComponent(e.name)},body:e});if(!r.ok){let e=await jF(r);n.updateAttachment(t,{status:`error`,error:`${r.status}: ${e}`});return}let i=await r.json();n.updateAttachment(t,{status:`ready`,path:i.path,size:i.size})}catch(e){n.updateAttachment(t,{status:`error`,error:e instanceof Error?e.message:String(e)})}}async function jF(e){try{return(await e.text()).slice(0,200)}catch{return`(no body)`}}function MF({attachment:e,rawUrl:t}){let n=t(e.path),r=e.name??e.path;return(0,J.jsx)(`a`,{href:n,target:`_blank`,rel:`noreferrer`,title:r,className:`overflow-hidden rounded-md border border-gray-800 bg-gray-900/60`,children:(0,J.jsx)(`img`,{src:n,alt:r,className:`block max-h-48 max-w-[16rem] object-contain`,loading:`lazy`})})}function NF({attachment:e}){let t=e.name??e.path;return(0,J.jsxs)(`div`,{className:`flex items-center gap-1.5 rounded-md border border-gray-800 bg-gray-900/60 px-2 py-1 text-xs text-gray-200`,title:e.path,children:[(0,J.jsx)(sh,{size:12,className:`text-gray-400`}),(0,J.jsx)(`span`,{className:`max-w-[12rem] truncate`,children:t}),(0,J.jsx)(`span`,{className:`text-[10px] text-gray-500`,children:wF(e.size??0)})]})}uF({open(e){window.dispatchEvent(new CustomEvent(`tianshu:files:open`,{detail:{path:e}}))}});var PF={components:{FilesPanel:EF,UploadButton:kF,ImageAttachment:MF,FileAttachment:NF}};PF.components;var FF=`/api/p/microsandbox`;async function IF(e,t){let n=await fetch(e,{credentials:`include`,...t}),r=await n.text(),i=null;if(r)try{i=JSON.parse(r)}catch{}if(!n.ok){let e=i?.message??i?.error??`HTTP ${n.status}`;throw Error(e)}return i}function LF(e){let t=Date.parse(e);if(Number.isNaN(t))return e;let n=Date.now()-t,r=Math.floor(n/6e4);if(r<1)return`just now`;if(r<60)return`${r}m ago`;let i=Math.floor(r/60);return i<24?`${i}h ago`:`${Math.floor(i/24)}d ago`}function RF(e){if(e<1e3)return`${e}ms`;let t=Math.floor(e/1e3);if(t<60)return`${t}s`;let n=Math.floor(t/60);return n<60?`${n}m ${t%60}s`:`${Math.floor(n/60)}h ${n%60}m`}function zF(e){if(!Number.isFinite(e)||e<0)return`—`;if(e<1024)return`${e} KiB`;let t=e/1024;return t<1024?`${Math.round(t)} MiB`:`${(t/1024).toFixed(1)} GiB`}function BF(e,t){if(!e)return null;let n=e[t];return typeof n==`string`?n:null}function VF(e){let[t,n]=(0,S.useState)(0),r=(0,S.useCallback)(()=>n(e=>e+1),[]);return(0,J.jsxs)(`div`,{className:`mx-auto max-w-4xl px-6 py-6 text-gray-200`,children:[(0,J.jsxs)(`header`,{className:`mb-6 border-b border-gray-800 pb-4`,children:[(0,J.jsx)(`h1`,{className:`text-lg font-semibold text-gray-100`,children:`MicroSandbox`}),(0,J.jsx)(`p`,{className:`mt-1 text-[12px] leading-relaxed text-gray-500`,children:`Edit your Sandboxfile, build a new image, and switch this tenant to use it. Sanity-check a fresh build via the shell's preview target (“does my apt package show up on PATH?”) before flipping the tenant to it.`})]}),(0,J.jsx)(HF,{}),(0,J.jsx)(`div`,{className:`my-6 border-t border-gray-800`}),(0,J.jsx)(UF,{onMutate:r}),(0,J.jsx)(`div`,{className:`my-6 border-t border-gray-800`}),(0,J.jsx)(qF,{refreshTick:t}),(0,J.jsx)(`div`,{className:`my-6 border-t border-gray-800`}),(0,J.jsx)(WF,{}),(0,J.jsx)(`div`,{className:`my-6 border-t border-gray-800`}),(0,J.jsx)(KF,{refreshTick:t,onMutate:r})]})}function HF(){let[e,t]=(0,S.useState)(null),[n,r]=(0,S.useState)(``),[i,a]=(0,S.useState)(!0),[o,s]=(0,S.useState)(!1),[c,l]=(0,S.useState)(null),[u,d]=(0,S.useState)(null),[f,p]=(0,S.useState)(null),[m,h]=(0,S.useState)([]),g=(0,S.useCallback)(async()=>{a(!0),l(null);try{let e=await IF(`${FF}/sandboxfile`);t(e),r(e.content)}catch(e){l(e instanceof Error?e.message:String(e))}finally{a(!1)}},[]);(0,S.useEffect)(()=>{g()},[g]),(0,S.useEffect)(()=>{let e=!1;return(async()=>{try{let t=await IF(`${FF}/sandboxfile/templates`);e||h(t.templates)}catch{}})(),()=>{e=!0}},[]);let _=e!=null&&n!==e.content;function v(t){if(!t)return;let i=m.find(e=>e.id===t);i&&(e&&n!==e.content&&!window.confirm(`You have unsaved changes. Replace the editor with the "${i.displayName}" template?`)||r(i.content))}async function y(){s(!0),l(null),d(null);try{let e=await IF(`${FF}/sandboxfile`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({content:n})});t({content:n,exists:!0,path:e.path}),d(e.parseError),p(Date.now())}catch(e){l(e instanceof Error?e.message:String(e))}finally{s(!1)}}return(0,J.jsxs)(`section`,{children:[(0,J.jsx)(ZF,{title:`Sandboxfile`,description:e?.exists===!1?`No Sandboxfile yet. Edit and save to create one.`:e?.path?`Saved at ${e.path}`:``,actions:(0,J.jsxs)(J.Fragment,{children:[m.length>0&&(0,J.jsxs)(`select`,{value:``,onChange:e=>{v(e.target.value),e.target.value=``},className:`rounded-md border border-gray-800 bg-gray-900 px-2 py-1 text-[11px] text-gray-300 hover:border-gray-700 focus:border-blue-700 focus:outline-none`,title:`Replace the editor contents with a starting template`,children:[(0,J.jsx)(`option`,{value:``,children:`Load template…`}),m.map(e=>(0,J.jsx)(`option`,{value:e.id,title:e.description,children:e.displayName},e.id))]}),(0,J.jsxs)(`button`,{type:`button`,onClick:()=>void g(),disabled:i,className:`btn-ghost flex items-center gap-1.5 px-2 py-1 text-[11px] text-gray-400`,title:`Reload from disk`,children:[(0,J.jsx)(ZT,{size:12,className:i?`animate-spin`:``}),`Reload`]}),(0,J.jsxs)(`button`,{type:`button`,onClick:()=>void y(),disabled:!_||o,className:`flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-[11px] font-medium text-white hover:bg-blue-500 disabled:cursor-not-allowed disabled:bg-gray-800 disabled:text-gray-500`,children:[o?(0,J.jsx)(db,{size:12,className:`animate-spin`}):(0,J.jsx)(BE,{size:12}),`Save`]})]})}),c&&(0,J.jsx)(QF,{kind:`error`,text:c}),u&&(0,J.jsx)(QF,{kind:`warn`,text:`Saved, but the file does not parse: ${u}`}),!c&&!u&&f&&Date.now()-f<4e3&&(0,J.jsx)(QF,{kind:`ok`,text:`Saved.`}),(0,J.jsx)(`textarea`,{value:n,onChange:e=>r(e.target.value),spellCheck:!1,className:`mt-2 h-64 w-full resize-y rounded-md border border-gray-800 bg-gray-950 px-3 py-2 font-mono text-[12px] leading-relaxed text-gray-200 outline-none focus:border-blue-700`,placeholder:`image: python:3.12-slim
25
+ - The chat UI knows how to render those; absolute host paths or bare "./foo" links won't preview.`},{id:`edit-vs-write`,text:"- `edit_file` is for changes to an existing file (in-place exact-text replace, atomic). `write_file` is for new files or full rewrites of the entire file. Prefer editing existing files; do not create new files unless asked."},{id:`edit-old-text-uniqueness`,text:'- Each `edit_file` `old_text` must appear exactly once in the file. Keep `old_text` as small as possible while still being unique — don\'t pad with large unchanged regions to "connect" two distant changes; emit two separate edits in the same `edits[]` batch instead.'},{id:`read-before-edit`,text:"- Before `edit_file` (or `write_file` overwrite) on a file you haven't seen yet in this session, call `read_file` once. The host tracks reads per session, so a single full-file read covers any number of follow-up `edit_file` / `write_file` calls on that same path — don't re-read between edits. Files you just created with `write_file` count as already-read; you can `edit_file` them right away without reading first."}]}},rF={id:`microsandbox`,version:`0.1.0`,displayName:`MicroSandbox`,description:`Per-tenant shell sandbox running on microsandbox (Apple Virtualization.framework / KVM). Provides sandbox.shell + an opt-in browser.cdp sidecar (Stealth Chromium via CloakBrowser + Playwright MCP + noVNC; lit up by a follow-up PR).`,author:`tianshu-ai`,license:`Apache-2.0`,permissions:[`workspace.read`,`workspace.write`,`process.spawn`],provides:[`sandbox.shell`,`sandbox.taskPool`,`browser.cdp`],client:{entry:`@tianshu-builtin/plugin-microsandbox/client`},server:{entry:`@tianshu-builtin/plugin-microsandbox/server`},contributes:{topBarButtons:[{id:`browser`,icon:`Globe`,tooltip:`Browser viewport (noVNC)`,opensPanel:`microsandbox.browser`,order:210}],rightPanels:[{id:`browser`,displayName:`Browser`,component:`BrowserViewportPanel`}],adminPages:[{id:`main`,displayName:`Sandbox`,icon:`Box`,component:`MicroSandboxAdminPage`,group:`Plugins`,order:100}],sandboxes:[{id:`main`,kind:`shell`,displayName:`MicroSandbox`,module:`MicroSandboxRunner`}],tools:[{id:`exec`,module:`ExecTool`},{id:`reset_sandbox`,module:`ResetSandboxTool`},{id:`get_sandbox_status`,module:`GetSandboxStatusTool`},{id:`update_sandbox_config`,module:`UpdateSandboxConfigTool`},{id:`build_sandbox`,module:`BuildSandboxTool`},{id:`list_sandbox_builds`,module:`ListSandboxBuildsTool`},{id:`use_sandbox_build`,module:`UseSandboxBuildTool`},{id:`browser_health_check`,module:`BrowserHealthCheckTool`}],toolsets:[{id:`browser`,module:`BrowserToolset`,displayName:`Playwright (Browser)`}],skills:[{id:`exec-howto`,path:`skills/microsandbox-exec-howto.md`},{id:`config`,path:`skills/microsandbox-config.md`},{id:`build-use`,path:`skills/microsandbox-build-use.md`},{id:`browser-howto`,path:`skills/microsandbox-browser-howto.md`},{id:`libreoffice`,path:`skills/microsandbox-libreoffice.md`}],systemPromptFragments:[{id:`no-foreground-servers`,text:"- Long-running servers (`python -m http.server`, `npm start`, `npm run dev`, anything that listens on a port and never returns) WILL hang the `exec` call until the host's per-call timeout fires (5 min default). Even with `nohup` alone the call hangs because microsandbox's exec channel waits for the started process tree's stdio to close, and nohup doesn't close stdin or detach from the controlling terminal.\n\n If you need to start a server, the ONLY safe pattern is:\n\n nohup setsid <cmd> > /tmp/srv.log 2>&1 < /dev/null &\n\n All four pieces are required: `setsid` puts the process in a new session (no controlling terminal); `< /dev/null` closes stdin; `>/tmp/srv.log 2>&1` redirects stdout AND stderr; trailing `&` puts it in the shell background. Skip any one and exec hangs.\n\n Then verify with `curl http://localhost:<port>/...` in a separate exec call. Don't background-then-curl in the same shell line — the shell process itself becomes the parent and won't exit."},{id:`already-installed-runtimes`,text:"- The sandbox already ships chromium (CDP 9222), Playwright MCP (3200), LibreOffice, Node 22, and Python 3.12. Don't `apt install` / `pip install` / `playwright install` what's already there."},{id:`task-sandbox-is-ephemeral`,text:"- If you are running as a workboard task worker (you have a workboard task id and your run will end with `task_complete`), the sandbox you `exec` in is per-task and gets stopped the moment the task terminates. Anything you nohup / setsid / disown into the background dies with the sandbox — don't try to leave servers / queues / cron jobs / file watchers running for someone else to consume. Run the work inline, capture its output to a file under your home, then `task_complete` with the file path.\n\n Concrete VERIFY-task pattern: if the deliverable is an HTML / PDF / image file and you want to confirm it renders, DON'T spin up `python -m http.server` to serve it. Either (a) use a Playwright MCP `browser_navigate` to `file:///workspace/users/<self>/projects/<slug>/report.html` directly (Playwright MCP is configured with `--allow-unrestricted-file-access`, so file:// URLs work), or (b) `cat` / `head` the file and inspect the structure inline, or (c) for charts, check the rendered file's size + that it contains expected canvas/svg/img markup (`grep -c '<canvas\\|<svg\\|<img' report.html`). Spinning up a transient http server adds a process that has to be cleanly torn down before task_complete, and the typical agent forgets the `setsid + </dev/null` pattern and hangs the run.\n\n Long-lived background services belong in a chat session (the long-lived sandbox there survives between turns), not in a task. Installed packages, files you wrote, and other on-disk state DO survive across attempts of the same task — only running processes don't."}],apiRoutes:[{method:`GET`,path:`/status`,handler:`status`},{method:`GET`,path:`/sandboxfile`,handler:`getSandboxfile`},{method:`PUT`,path:`/sandboxfile`,handler:`putSandboxfile`},{method:`GET`,path:`/sandboxfile/templates`,handler:`getSandboxfileTemplates`},{method:`GET`,path:`/builds`,handler:`getBuilds`},{method:`POST`,path:`/builds`,handler:`postBuilds`},{method:`POST`,path:`/builds/use`,handler:`postUseBuild`},{method:`POST`,path:`/reset`,handler:`postReset`},{method:`POST`,path:`/exec`,handler:`postExec`},{method:`GET`,path:`/task-pool`,handler:`getTaskPool`},{method:`POST`,path:`/task-pool/destroy`,handler:`postTaskPoolDestroy`},{method:`GET`,path:`/browser/status`,handler:`getBrowserStatus`},{method:`POST`,path:`/browser/restart`,handler:`postBrowserRestart`},{method:`POST`,path:`/browser/resize`,handler:`postBrowserResize`}]},configSchema:{fields:[{key:`memoryMib`,label:`Browser sandbox memory`,kind:`number`,unit:`MiB`,min:512,max:65536,step:512,default:2048,description:`RAM allotted to the long-lived Browser sandbox VM. Increase to 8192+ MiB for browser-heavy workloads (chromium + Playwright MCP), 12288+ for build-time pip / npm of large stacks. Restart the sandbox (Reset) for the new value to take effect.`},{key:`cpus`,label:`Browser sandbox vCPU count`,kind:`number`,min:1,max:16,step:1,default:2,description:`Logical CPU count handed to the long-lived Browser sandbox VM. Reset the sandbox after changing.`},{key:`execTimeoutMs`,label:`Per-exec timeout (Browser)`,kind:`number`,unit:`ms`,min:5e3,max:18e5,step:5e3,default:3e5,description:"Max wall-clock time for one Browser `exec` call before it's force-killed. Bump this for long-running installers; consider running them with `nohup setsid … &` instead."},{key:`taskMemoryMib`,label:`Task sandbox memory`,kind:`number`,unit:`MiB`,min:512,max:65536,step:512,default:2048,description:`RAM allotted to per-task sandbox VMs (forked from the Task snapshot). Set lower than the Browser value to fit more parallel tasks; raise it for tasks that need a heavy data-science stack. Effective on the next task acquire.`},{key:`taskCpus`,label:`Task sandbox vCPU count`,kind:`number`,min:1,max:16,step:1,default:2,description:`Logical CPU count handed to each per-task sandbox VM. Effective on the next task acquire.`},{key:`taskExecTimeoutMs`,label:`Per-exec timeout (Task)`,kind:`number`,unit:`ms`,min:5e3,max:18e5,step:5e3,default:3e5,description:"Max wall-clock time for one Task `exec` call before it's force-killed. Per-task sandboxes typically run shorter, more focused commands than the Browser sandbox."},{key:`taskIdleShutdownMs`,label:`Task sandbox idle shutdown`,kind:`number`,unit:`ms`,min:0,max:864e5,step:6e4,default:6e5,description:`How long a task sandbox stays alive after its task ends before it's stopped. 0 = stop immediately on release. Higher values trade RAM for faster reuse when the same kind of task runs again soon.`}]}},iF={id:`web-search`,version:`0.1.0`,displayName:`Web Search`,description:`Web search via Tavily and Brave Search APIs. One tool, two providers, automatic fallback. Set API keys in Settings → Plugins.`,author:`tianshu-ai`,license:`Apache-2.0`,permissions:[`network.external`],server:{entry:`@tianshu-builtin/plugin-web-search/server`},contributes:{tools:[{id:`web_search`,module:`WebSearchTool`}],skills:[{id:`web-search-howto`,path:`skills/web-search-howto.md`}],apiRoutes:[{method:`GET`,path:`/health`,handler:`getHealth`},{method:`POST`,path:`/health/reset`,handler:`resetHealth`}]},configSchema:{fields:[{key:`tavilyApiKey`,label:`Tavily API key`,kind:`secret`,placeholder:`tvly-...`,description:`Sign up at tavily.com (free tier 1000 searches/month). Stored under <tenant>/secrets/, never returned to the browser.`},{key:`braveApiKey`,label:`Brave Search API key`,kind:`secret`,placeholder:`BSA...`,description:`Optional. Get a free key at api-dashboard.search.brave.com (2000/month). Used as fallback when Tavily fails or returns nothing.`},{key:`preferredProvider`,label:`Preferred provider`,kind:`select`,default:`tavily`,options:[{label:`Tavily`,value:`tavily`},{label:`Brave`,value:`brave`}],description:`Tried first when both keys are set. The other provider is used as automatic fallback only on transient errors (network / 5xx / 429). Auth failures (401 / 403) park the bad key in the health cache; clear it from Settings after fixing the key.`},{key:`timeoutMs`,label:`Per-request timeout`,kind:`number`,unit:`ms`,min:1e3,max:6e4,step:500,default:8e3,description:`Network timeout for each provider call. Increase if you hit timeouts on slow networks.`}]}},aF={id:`workboard`,version:`0.1.0`,displayName:`Workboard`,description:`Kanban task board with a worker pool. The orchestrator drops tasks onto the board; workers pick them up, run them, and report back. v0.2 ships an echo worker so the full ready→in-progress→done loop is visible end-to-end; real worker runtimes (qianliyan / luban / xihe / nvwa) land in follow-up PRs as separate plugins.`,author:`tianshu-ai`,license:`Apache-2.0`,permissions:[`workspace.read`,`tenant.db`],requires:[`host.agentLoop`,`host.sessionInbox`,`host.toolCatalog`,`host.skillCatalog`,`host.modelCatalog`],client:{entry:`@tianshu-builtin/plugin-workboard/client`},server:{entry:`@tianshu-builtin/plugin-workboard/server`},contributes:{topBarButtons:[{id:`toggle`,icon:`Kanban`,tooltip:`Task board`,opensPanel:`workboard.main`,order:200}],rightPanels:[{id:`main`,displayName:`Tasks`,component:`WorkboardPanel`}],sidebarSections:[{id:`workers`,displayName:`Workers`,component:`WorkersSidebarSection`,after:`workers`,order:100}],adminPages:[{id:`agents`,displayName:`Worker agents`,icon:`Bot`,component:`WorkerAgentsPage`,order:10}],tools:[{id:`task_list`,module:`TaskListTool`},{id:`task_create`,module:`TaskCreateTool`},{id:`task_update`,module:`TaskUpdateTool`},{id:`task_move`,module:`TaskMoveTool`},{id:`task_delete`,module:`TaskDeleteTool`},{id:`task_get_history`,module:`TaskGetHistoryTool`},{id:`task_complete`,module:`TaskCompleteTool`},{id:`task_continue`,module:`TaskContinueTool`},{id:`task_retry_fresh`,module:`TaskRetryFreshTool`},{id:`task_extend_timeout`,module:`TaskExtendTimeoutTool`},{id:`task_abort`,module:`TaskAbortTool`},{id:`model_list`,module:`ModelListTool`},{id:`worker_analytics`,module:`WorkerAnalyticsTool`}],skills:[{id:`workboard-howto`,path:`skills/workboard-howto.md`},{id:`worker-creator`,path:`skills/worker-creator.md`},{id:`worker-fleet`,path:`skills/worker-fleet.md`},{id:`large-input-large-output`,path:`skills/large-input-large-output.md`}],agentSeeds:[{id:`echo-demo`,path:`agent-seeds/echo-demo`},{id:`llm-default`,path:`agent-seeds/llm-default`}],systemPromptFragments:[{id:`prefer-delegation`,text:'The workboard is active in this tenant. For any non-trivial user request — anything that takes more than a few minutes, fans out into multiple steps, or has clearly separable roles (design / build / test, research / write, etc.) — your default is to DELEGATE to worker agents via `task_create` rather than execute it yourself. The `worker-fleet` skill covers task decomposition (plan / design / build / verify phases), task sizing (avoiding max_tokens stalls on monolithic tasks), worker selection (matching by capability), and brief authoring (each task description is the worker\'s only view of the work) — read it before designing the task graph. Reuse existing workers (run `tenant_config_list({path:"workers"})` first); the `worker-creator` skill covers authoring agent.json + SOUL.md when no existing worker fits. Do the work yourself directly only when (a) the user explicitly says so, (b) the request is trivial (single tool call, one-line answer, quick lookup), or (c) the user wants a synchronous reply. When in doubt, ack the plan and give the user a chance to override.'},{id:`summarize-on-completion`,text:"When the workboard tasks you delegated for the user's current request have all finished, your job isn't done — you owe the user a synthesised reply. Steps: (1) Call `task_list({status:\"done\"})` (or filter by labels/ids you tracked) to confirm every task you created in this turn has actually completed; if any are still running, wait or fetch their status before composing. (2) For each completed task, call `task_get_history({id})` and READ the worker's actual output — don't trust `resultSummary` alone; workers sometimes write a one-liner that hides nuance, errors, or important findings. Skim the final assistant message + any tool results that produced files. (3) Compose ONE coherent reply to the user that: covers the original goal, integrates the per-task findings into a narrative (not a bulleted dump of resultSummaries), explicitly cites every output file the workers produced (use the file's path, and where the tenant has the `files` plugin enabled use the standard file-reference markup the user's chat client understands so they can click through), and surfaces any failures / partial results honestly. (4) If a task ended in awaiting-intervention, do NOT just report the failure — either resolve it yourself (task_continue / task_retry_fresh / task_extend_timeout / task_abort) before replying, or include in the reply a concrete suggestion for what the user should choose. Keep the synthesis honest: if outputs contradict each other or a worker punted, say so; don't paper over with cheerful language."}],apiRoutes:[{method:`GET`,path:`/tasks`,handler:`listTasks`},{method:`POST`,path:`/tasks`,handler:`createTask`},{method:`POST`,path:`/tasks/delete`,handler:`deleteTask`},{method:`PATCH`,path:`/tasks/:id`,handler:`patchTask`},{method:`GET`,path:`/tasks/:id/history`,handler:`taskHistory`},{method:`GET`,path:`/projects`,handler:`listProjects`},{method:`GET`,path:`/workers/status`,handler:`workerStatus`},{method:`POST`,path:`/workers/restart`,handler:`workerRestart`},{method:`GET`,path:`/agents`,handler:`listAgents`},{method:`PATCH`,path:`/agents/:slug/enabled`,handler:`setAgentEnabled`}]},configSchema:{fields:[{key:`echo.enabled`,label:`Enabled`,kind:`boolean`,default:!0,group:{id:`worker-type-echo`,label:`echo`,badge:`worker type`}},{key:`echo.delayMs`,label:`Simulated execution delay`,description:`How long an echo worker pretends to be busy on each task before marking it done. 0 makes tasks complete instantly.`,kind:`number`,default:3e4,min:0,step:1e3,unit:`ms`,group:{id:`worker-type-echo`,label:`echo`,badge:`worker type`}},{key:`llm.enabled`,label:`Enabled`,kind:`boolean`,default:!0,group:{id:`worker-type-llm`,label:`llm`,badge:`worker type`}},{key:`llm.firstResponseSec`,label:`First-response timeout`,description:`Hard cap on how long the LLM has to start streaming the very first event of the run. Catches connection-hung scenarios. Set to 0 to disable.`,kind:`number`,default:300,min:0,step:30,unit:`s`,group:{id:`worker-type-llm`,label:`llm`,badge:`worker type`}},{key:`llm.idleSec`,label:`Idle timeout`,description:"Mid-stream silence cap: if the LLM goes this long without emitting any event, abort the run as stuck. Long enough to allow large `write_file` arguments to stream through.",kind:`number`,default:600,min:0,step:60,unit:`s`,group:{id:`worker-type-llm`,label:`llm`,badge:`worker type`}},{key:`llm.maxRunSec`,label:`Max run time`,description:`Total wall-clock cap for one task run, across all turns. Protects against runaway loops.`,kind:`number`,default:1800,min:0,step:60,unit:`s`,group:{id:`worker-type-llm`,label:`llm`,badge:`worker type`}}]}},oF=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),J=o(((e,t)=>{t.exports=oF()}))();function sF(){return globalThis}function cF(e){sF().__tianshuPluginSdkComposer__=e}function lF(){return globalThis}function uF(e){lF().__tianshuPluginSdkOpenFile__=e}function dF(){return globalThis}function fF(e){dF().__tianshuPluginSdkPluginConfigForm__=e}var pF=e=>{let t=dF().__tianshuPluginSdkPluginConfigForm__;return t?S.createElement(t,e):null};function mF(){return e=>{let t=lF().__tianshuPluginSdkOpenFile__;if(!t){let t=e.replace(/^workspace:\/\/+/,`/`),n=`/api/p/files/raw?path=${encodeURIComponent(t)}`;window.open(n,`_blank`,`noopener`);return}t.open(e)}}var hF=`/api/p/files`,gF=new Set([`.png`,`.jpg`,`.jpeg`,`.gif`,`.webp`,`.bmp`,`.ico`,`.svg`]),_F=new Set([`.js`,`.cjs`,`.mjs`,`.ts`,`.jsx`,`.tsx`,`.py`,`.rb`,`.go`,`.rs`,`.c`,`.cpp`,`.h`,`.java`,`.kt`,`.swift`,`.sh`,`.bash`,`.zsh`,`.lua`,`.php`,`.pl`,`.vue`,`.svelte`]),vF=new Set([`.json`,`.yaml`,`.yml`,`.toml`,`.xml`,`.csv`,`.tsv`]),yF=new Set([`.mp4`,`.avi`,`.mov`,`.mkv`,`.webm`,`.m4v`]),bF=new Set([`.mp3`,`.wav`,`.ogg`,`.flac`,`.aac`,`.m4a`]),xF=new Set([`.zip`,`.tar`,`.gz`,`.tgz`,`.rar`,`.7z`,`.bz2`]),SF=new Set([`.md`,`.markdown`,`.txt`,`.log`,`.html`,`.css`,`.env`]);function CF(e){if(e.type===`directory`)return(0,J.jsx)($h,{size:16,className:`text-yellow-500`});let t=(e.extension??``).toLowerCase();return gF.has(t)?(0,J.jsx)(Fv,{size:16,className:`text-green-400`}):_F.has(t)?(0,J.jsx)(Vd,{size:16,className:`text-blue-400`}):vF.has(t)?(0,J.jsx)(Jm,{size:16,className:`text-orange-400`}):yF.has(t)?(0,J.jsx)(uh,{size:16,className:`text-purple-400`}):bF.has(t)?(0,J.jsx)(YS,{size:16,className:`text-pink-400`}):xF.has(t)?(0,J.jsx)(Ci,{size:16,className:`text-gray-400`}):SF.has(t)?(0,J.jsx)(Qm,{size:16,className:`text-gray-300`}):(0,J.jsx)(sh,{size:16,className:`text-gray-500`})}function wF(e){return e===0?`—`:e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MB`:`${(e/(1024*1024*1024)).toFixed(1)} GB`}function TF(e){if(e===0)return`—`;let t=Date.now()-e;return t<6e4?`Just now`:t<36e5?`${Math.floor(t/6e4)}m ago`:t<864e5?`${Math.floor(t/36e5)}h ago`:t<7*864e5?`${Math.floor(t/864e5)}d ago`:new Date(e).toLocaleDateString()}function EF({plugin:e}){let[t,n]=(0,S.useState)(`/`),[r,i]=(0,S.useState)(null),[a,o]=(0,S.useState)(!1),[s,c]=(0,S.useState)(null),[l,u]=(0,S.useState)(``),[d,f]=(0,S.useState)(`name`),[p,m]=(0,S.useState)(`asc`),[h,g]=(0,S.useState)(`list`),[_,v]=(0,S.useState)(null),y=(0,S.useCallback)(async e=>{o(!0),c(null);try{let t=await fetch(`${hF}/list?dir=${encodeURIComponent(e)}`,{credentials:`include`});if(!t.ok)throw Error(`${t.status}`);i(await t.json())}catch(e){c(e instanceof Error?e.message:String(e)),i(null)}finally{o(!1)}},[]);(0,S.useEffect)(()=>{y(t)},[t,y]);let b=(0,S.useMemo)(()=>{if(!r)return[];let e=[...r.entries];return e.sort((e,t)=>{if(e.type!==t.type)return e.type===`directory`?-1:1;let n=0;return n=d===`name`?e.name.localeCompare(t.name):d===`size`?e.size-t.size:e.modifiedMs-t.modifiedMs,p===`asc`?n:-n}),e},[r,d,p]),x=(0,S.useMemo)(()=>{if(!l)return b;let e=l.toLowerCase();return b.filter(t=>t.name.toLowerCase().includes(e))},[b,l]),C=e=>{u(``),n(e)};function w(e){d===e?m(e=>e===`asc`?`desc`:`asc`):(f(e),m(`asc`))}let T=t===`/`?[`/`]:[`/`,...t.split(`/`).filter(Boolean)];return(0,J.jsxs)(`div`,{className:`flex h-full flex-col bg-gray-950 text-gray-300`,children:[(0,J.jsxs)(`div`,{className:`space-y-2 border-b border-gray-800 px-3 py-2`,children:[(0,J.jsxs)(`div`,{className:`flex items-center gap-1 overflow-x-auto text-sm`,children:[t!==`/`&&(0,J.jsx)(`button`,{type:`button`,onClick:()=>C(t.substring(0,t.lastIndexOf(`/`))||`/`),className:`mr-1 shrink-0 text-gray-500 hover:text-gray-300`,title:`Up one level`,children:(0,J.jsx)(Ji,{size:14})}),T.map((e,t)=>{let n=t===0?`/`:`/`+T.slice(1,t+1).join(`/`),r=t===T.length-1;return(0,J.jsxs)(`span`,{className:`flex shrink-0 items-center`,children:[t>0&&(0,J.jsx)(zl,{size:12,className:`mx-0.5 text-gray-600`}),(0,J.jsx)(`button`,{type:`button`,onClick:()=>C(n),className:[`rounded px-1 hover:text-white`,r?`font-medium text-white`:`text-gray-500`].join(` `),children:t===0?`~`:e})]},t)})]}),(0,J.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,J.jsxs)(`div`,{className:`relative flex-1`,children:[(0,J.jsx)(fD,{size:14,className:`pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-500`}),(0,J.jsx)(`input`,{type:`text`,value:l,onChange:e=>u(e.target.value),placeholder:`Filter files...`,className:`w-full rounded border border-gray-800 bg-gray-900 py-1 pl-8 pr-2 text-sm text-gray-300 placeholder-gray-600 focus:border-gray-600 focus:outline-none`})]}),(0,J.jsx)(`button`,{type:`button`,onClick:()=>y(t),className:`rounded p-1.5 text-gray-400 hover:bg-gray-800 hover:text-gray-200`,title:`Refresh`,children:(0,J.jsx)(ZT,{size:14,className:a?`animate-spin`:``})}),(0,J.jsx)(`button`,{type:`button`,onClick:()=>g(h===`list`?`grid`:`list`),className:`rounded p-1.5 text-gray-400 hover:bg-gray-800 hover:text-gray-200`,title:`Toggle view`,children:h===`list`?(0,J.jsx)(Sy,{size:14}):(0,J.jsx)(wy,{size:14})})]})]}),h===`list`&&(0,J.jsxs)(`div`,{className:`flex select-none items-center border-b border-gray-800/50 px-3 py-1 text-xs text-gray-500`,children:[(0,J.jsxs)(`button`,{type:`button`,onClick:()=>w(`name`),className:`flex flex-1 items-center gap-1 text-left hover:text-gray-300`,children:[`Name `,d===`name`&&(0,J.jsx)(na,{size:10})]}),(0,J.jsxs)(`button`,{type:`button`,onClick:()=>w(`size`),className:`flex w-20 items-center justify-end gap-1 text-right hover:text-gray-300`,children:[`Size `,d===`size`&&(0,J.jsx)(na,{size:10})]}),(0,J.jsxs)(`button`,{type:`button`,onClick:()=>w(`modified`),className:`flex w-24 items-center justify-end gap-1 text-right hover:text-gray-300`,children:[`Modified `,d===`modified`&&(0,J.jsx)(na,{size:10})]})]}),(0,J.jsxs)(`div`,{className:`flex-1 overflow-auto`,children:[a&&!r?(0,J.jsx)(`div`,{className:`flex h-32 items-center justify-center`,children:(0,J.jsx)(db,{size:20,className:`animate-spin text-gray-500`})}):s?(0,J.jsx)(`div`,{className:`p-4 text-center text-sm text-rose-400`,children:s}):x.length===0?(0,J.jsx)(`div`,{className:`p-8 text-center text-sm text-gray-600`,children:l?`No matching files`:`Empty directory`}):h===`list`?(0,J.jsx)(`ul`,{className:`divide-y divide-gray-800/30`,children:x.map(e=>(0,J.jsxs)(`li`,{className:`group flex cursor-pointer items-center px-3 py-1.5 hover:bg-gray-800/50`,onClick:()=>{e.type===`directory`?C(e.path):v(e)},children:[(0,J.jsxs)(`div`,{className:`flex min-w-0 flex-1 items-center gap-2`,children:[CF(e),(0,J.jsx)(`span`,{className:`truncate text-sm`,children:e.name})]}),(0,J.jsx)(`span`,{className:`w-20 text-right text-xs text-gray-500`,children:e.type===`file`?wF(e.size):`—`}),(0,J.jsx)(`span`,{className:`w-24 text-right text-xs text-gray-500`,children:TF(e.modifiedMs)})]},e.path))}):(0,J.jsx)(`div`,{className:`grid grid-cols-3 gap-2 p-3`,children:x.map(e=>(0,J.jsxs)(`button`,{type:`button`,onClick:()=>{e.type===`directory`?C(e.path):v(e)},className:`flex cursor-pointer flex-col items-center rounded-lg p-3 hover:bg-gray-800/50`,children:[(0,J.jsx)(`div`,{className:`mb-1`,children:CF(e)}),(0,J.jsx)(`span`,{className:`w-full truncate text-center text-xs`,children:e.name})]},e.path))}),r?.truncated&&(0,J.jsx)(`div`,{className:`px-3 py-2 text-[10px] text-amber-400`,children:`Listing truncated at 5000 entries.`})]}),_&&(0,J.jsx)(DF,{entry:_,onClose:()=>v(null)})]})}function DF({entry:e,onClose:t}){let[n,r]=(0,S.useState)(null),[i,a]=(0,S.useState)(null),o=(e.extension??``).toLowerCase(),s=gF.has(o);(0,S.useEffect)(()=>{if(s){r({path:e.path,size:e.size,modifiedMs:e.modifiedMs,binary:!0});return}let t=!1;return a(null),fetch(`${hF}/read?path=${encodeURIComponent(e.path)}`,{credentials:`include`}).then(async e=>{if(!e.ok){if(e.status===413){let t=await e.json();throw Error(`File is ${wF(t.size)}, exceeds the ${wF(t.maxBytes)} preview cap.`)}throw Error(`HTTP ${e.status}`)}return await e.json()}).then(e=>{t||r(e)}).catch(e=>{t||a(e instanceof Error?e.message:String(e))}),()=>{t=!0}},[e.path,e.size,e.modifiedMs,s]);let c=(0,S.useRef)(null);return(0,S.useEffect)(()=>{let e=e=>{e.key===`Escape`&&t()};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[t]),(0,J.jsx)(`div`,{ref:c,className:`fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-6 backdrop-blur-sm`,onClick:t,children:(0,J.jsxs)(`div`,{className:`flex h-full max-h-[80vh] w-full max-w-3xl flex-col overflow-hidden rounded-xl border border-gray-700 bg-gray-900 shadow-2xl`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`flex items-center justify-between border-b border-gray-800 px-4 py-2.5`,children:[(0,J.jsxs)(`div`,{className:`flex min-w-0 items-center gap-2`,children:[CF(e),(0,J.jsx)(`span`,{className:`truncate text-sm font-medium text-gray-100`,children:e.name}),(0,J.jsxs)(`span`,{className:`flex-shrink-0 text-[11px] text-gray-500`,children:[wF(e.size),` · `,TF(e.modifiedMs)]})]}),(0,J.jsx)(`button`,{type:`button`,onClick:t,className:`btn-ghost p-1.5`,"aria-label":`Close`,children:(0,J.jsx)(IP,{size:16})})]}),(0,J.jsx)(`div`,{className:`flex-1 overflow-auto bg-gray-950`,children:i?(0,J.jsx)(`div`,{className:`p-6 text-center text-sm text-rose-300`,children:i}):n?s?(0,J.jsx)(`img`,{src:`${hF}/raw?path=${encodeURIComponent(e.path)}`,alt:e.name,className:`mx-auto max-h-full max-w-full`}):n.binary?(0,J.jsxs)(`div`,{className:`p-6 text-center text-sm text-gray-500`,children:[`Binary file (`,wF(n.size),`). No preview available.`]}):(0,J.jsx)(`pre`,{className:`whitespace-pre-wrap break-all p-4 font-mono text-xs text-gray-200`,children:n.content}):(0,J.jsx)(`div`,{className:`flex h-32 items-center justify-center`,children:(0,J.jsx)(db,{size:20,className:`animate-spin text-gray-500`})})})]})})}var OF=50*1024*1024;function kF(e){let{composer:t}=e,n=(0,S.useRef)(null);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`button`,{type:`button`,onClick:()=>n.current?.click(),title:`Attach file`,"aria-label":`Attach file`,className:`rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-gray-700 hover:text-gray-200`,children:(0,J.jsx)(nw,{size:16})}),(0,J.jsx)(`input`,{ref:n,type:`file`,multiple:!0,className:`hidden`,onChange:e=>{let n=e.target.files?Array.from(e.target.files):[];e.target.value=``;for(let e of n){if(e.size>OF){t.addAttachment({name:e.name,size:e.size,status:`error`,error:`File exceeds ${(OF/1024/1024).toFixed(0)} MB cap`,mimeType:e.type||`application/octet-stream`});continue}AF(e,t.addAttachment({name:e.name,size:e.size,status:`uploading`,mimeType:e.type||`application/octet-stream`}),t)}}})]})}async function AF(e,t,n){try{let r=await fetch(`${hF}/upload`,{method:`POST`,headers:{"Content-Type":`application/octet-stream`,"X-Filename":encodeURIComponent(e.name)},body:e});if(!r.ok){let e=await jF(r);n.updateAttachment(t,{status:`error`,error:`${r.status}: ${e}`});return}let i=await r.json();n.updateAttachment(t,{status:`ready`,path:i.path,size:i.size})}catch(e){n.updateAttachment(t,{status:`error`,error:e instanceof Error?e.message:String(e)})}}async function jF(e){try{return(await e.text()).slice(0,200)}catch{return`(no body)`}}function MF({attachment:e,rawUrl:t}){let n=t(e.path),r=e.name??e.path;return(0,J.jsx)(`a`,{href:n,target:`_blank`,rel:`noreferrer`,title:r,className:`overflow-hidden rounded-md border border-gray-800 bg-gray-900/60`,children:(0,J.jsx)(`img`,{src:n,alt:r,className:`block max-h-48 max-w-[16rem] object-contain`,loading:`lazy`})})}function NF({attachment:e}){let t=e.name??e.path;return(0,J.jsxs)(`div`,{className:`flex items-center gap-1.5 rounded-md border border-gray-800 bg-gray-900/60 px-2 py-1 text-xs text-gray-200`,title:e.path,children:[(0,J.jsx)(sh,{size:12,className:`text-gray-400`}),(0,J.jsx)(`span`,{className:`max-w-[12rem] truncate`,children:t}),(0,J.jsx)(`span`,{className:`text-[10px] text-gray-500`,children:wF(e.size??0)})]})}uF({open(e){window.dispatchEvent(new CustomEvent(`tianshu:files:open`,{detail:{path:e}}))}});var PF={components:{FilesPanel:EF,UploadButton:kF,ImageAttachment:MF,FileAttachment:NF}};PF.components;var FF=`/api/p/microsandbox`;async function IF(e,t){let n=await fetch(e,{credentials:`include`,...t}),r=await n.text(),i=null;if(r)try{i=JSON.parse(r)}catch{}if(!n.ok){let e=i?.message??i?.error??`HTTP ${n.status}`;throw Error(e)}return i}function LF(e){let t=Date.parse(e);if(Number.isNaN(t))return e;let n=Date.now()-t,r=Math.floor(n/6e4);if(r<1)return`just now`;if(r<60)return`${r}m ago`;let i=Math.floor(r/60);return i<24?`${i}h ago`:`${Math.floor(i/24)}d ago`}function RF(e){if(e<1e3)return`${e}ms`;let t=Math.floor(e/1e3);if(t<60)return`${t}s`;let n=Math.floor(t/60);return n<60?`${n}m ${t%60}s`:`${Math.floor(n/60)}h ${n%60}m`}function zF(e){if(!Number.isFinite(e)||e<0)return`—`;if(e<1024)return`${e} KiB`;let t=e/1024;return t<1024?`${Math.round(t)} MiB`:`${(t/1024).toFixed(1)} GiB`}function BF(e,t){if(!e)return null;let n=e[t];return typeof n==`string`?n:null}function VF(e){let[t,n]=(0,S.useState)(0),r=(0,S.useCallback)(()=>n(e=>e+1),[]);return(0,J.jsxs)(`div`,{className:`mx-auto max-w-4xl px-6 py-6 text-gray-200`,children:[(0,J.jsxs)(`header`,{className:`mb-6 border-b border-gray-800 pb-4`,children:[(0,J.jsx)(`h1`,{className:`text-lg font-semibold text-gray-100`,children:`MicroSandbox`}),(0,J.jsx)(`p`,{className:`mt-1 text-[12px] leading-relaxed text-gray-500`,children:`Edit your Sandboxfile, build a new image, and switch this tenant to use it. Sanity-check a fresh build via the shell's preview target (“does my apt package show up on PATH?”) before flipping the tenant to it.`})]}),(0,J.jsx)(HF,{}),(0,J.jsx)(`div`,{className:`my-6 border-t border-gray-800`}),(0,J.jsx)(UF,{onMutate:r}),(0,J.jsx)(`div`,{className:`my-6 border-t border-gray-800`}),(0,J.jsx)(qF,{refreshTick:t}),(0,J.jsx)(`div`,{className:`my-6 border-t border-gray-800`}),(0,J.jsx)(WF,{}),(0,J.jsx)(`div`,{className:`my-6 border-t border-gray-800`}),(0,J.jsx)(KF,{refreshTick:t,onMutate:r})]})}function HF(){let[e,t]=(0,S.useState)(null),[n,r]=(0,S.useState)(``),[i,a]=(0,S.useState)(!0),[o,s]=(0,S.useState)(!1),[c,l]=(0,S.useState)(null),[u,d]=(0,S.useState)(null),[f,p]=(0,S.useState)(null),[m,h]=(0,S.useState)([]),g=(0,S.useCallback)(async()=>{a(!0),l(null);try{let e=await IF(`${FF}/sandboxfile`);t(e),r(e.content)}catch(e){l(e instanceof Error?e.message:String(e))}finally{a(!1)}},[]);(0,S.useEffect)(()=>{g()},[g]),(0,S.useEffect)(()=>{let e=!1;return(async()=>{try{let t=await IF(`${FF}/sandboxfile/templates`);e||h(t.templates)}catch{}})(),()=>{e=!0}},[]);let _=e!=null&&n!==e.content;function v(t){if(!t)return;let i=m.find(e=>e.id===t);i&&(e&&n!==e.content&&!window.confirm(`You have unsaved changes. Replace the editor with the "${i.displayName}" template?`)||r(i.content))}async function y(){s(!0),l(null),d(null);try{let e=await IF(`${FF}/sandboxfile`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({content:n})});t({content:n,exists:!0,path:e.path}),d(e.parseError),p(Date.now())}catch(e){l(e instanceof Error?e.message:String(e))}finally{s(!1)}}return(0,J.jsxs)(`section`,{children:[(0,J.jsx)(ZF,{title:`Sandboxfile`,description:e?.exists===!1?`No Sandboxfile yet. Edit and save to create one.`:e?.path?`Saved at ${e.path}`:``,actions:(0,J.jsxs)(J.Fragment,{children:[m.length>0&&(0,J.jsxs)(`select`,{value:``,onChange:e=>{v(e.target.value),e.target.value=``},className:`rounded-md border border-gray-800 bg-gray-900 px-2 py-1 text-[11px] text-gray-300 hover:border-gray-700 focus:border-blue-700 focus:outline-none`,title:`Replace the editor contents with a starting template`,children:[(0,J.jsx)(`option`,{value:``,children:`Load template…`}),m.map(e=>(0,J.jsx)(`option`,{value:e.id,title:e.description,children:e.displayName},e.id))]}),(0,J.jsxs)(`button`,{type:`button`,onClick:()=>void g(),disabled:i,className:`btn-ghost flex items-center gap-1.5 px-2 py-1 text-[11px] text-gray-400`,title:`Reload from disk`,children:[(0,J.jsx)(ZT,{size:12,className:i?`animate-spin`:``}),`Reload`]}),(0,J.jsxs)(`button`,{type:`button`,onClick:()=>void y(),disabled:!_||o,className:`flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-[11px] font-medium text-white hover:bg-blue-500 disabled:cursor-not-allowed disabled:bg-gray-800 disabled:text-gray-500`,children:[o?(0,J.jsx)(db,{size:12,className:`animate-spin`}):(0,J.jsx)(BE,{size:12}),`Save`]})]})}),c&&(0,J.jsx)(QF,{kind:`error`,text:c}),u&&(0,J.jsx)(QF,{kind:`warn`,text:`Saved, but the file does not parse: ${u}`}),!c&&!u&&f&&Date.now()-f<4e3&&(0,J.jsx)(QF,{kind:`ok`,text:`Saved.`}),(0,J.jsx)(`textarea`,{value:n,onChange:e=>r(e.target.value),spellCheck:!1,className:`mt-2 h-64 w-full resize-y rounded-md border border-gray-800 bg-gray-950 px-3 py-2 font-mono text-[12px] leading-relaxed text-gray-200 outline-none focus:border-blue-700`,placeholder:`image: python:3.12-slim
26
26
  cpus: 4
27
27
  memory_mib: 4096`}),(0,J.jsxs)(`p`,{className:`mt-2 text-[11px] leading-relaxed text-gray-500`,children:[`Path: `,(0,J.jsx)(`code`,{className:`rounded bg-gray-800 px-1`,children:e?.path??`…`}),`. v0 grammar: `,(0,J.jsx)(`code`,{className:`rounded bg-gray-800 px-1`,children:`image:`}),`,`,` `,(0,J.jsx)(`code`,{className:`rounded bg-gray-800 px-1`,children:`cpus:`}),`,`,` `,(0,J.jsx)(`code`,{className:`rounded bg-gray-800 px-1`,children:`memory_mib:`}),`, and the lists`,` `,(0,J.jsx)(`code`,{className:`rounded bg-gray-800 px-1`,children:`apt`}),`,`,` `,(0,J.jsx)(`code`,{className:`rounded bg-gray-800 px-1`,children:`pip`}),`,`,` `,(0,J.jsx)(`code`,{className:`rounded bg-gray-800 px-1`,children:`npm`}),`,`,` `,(0,J.jsx)(`code`,{className:`rounded bg-gray-800 px-1`,children:`exec`}),`.`]})]})}function UF({onMutate:e}){let[t,n]=(0,S.useState)(null),[r,i]=(0,S.useState)(null),[a,o]=(0,S.useState)(!0),[s,c]=(0,S.useState)(!1),[l,u]=(0,S.useState)(null),[d,f]=(0,S.useState)(``),[p,m]=(0,S.useState)([]),[h,g]=(0,S.useState)(null),[_,v]=(0,S.useState)(0),y=(0,S.useRef)(null);(0,S.useEffect)(()=>{if(h===null)return;let e=window.setInterval(()=>v(Date.now()-h),250);return()=>window.clearInterval(e)},[h]),(0,S.useEffect)(()=>{let e=y.current;e&&(e.scrollTop=e.scrollHeight)},[p]);let b=(0,S.useCallback)(async()=>{o(!0),i(null);try{n(await IF(`${FF}/builds`))}catch(e){i(e instanceof Error?e.message:String(e))}finally{o(!1)}},[]);(0,S.useEffect)(()=>{b()},[b]);async function x(){c(!0),i(null),m([]),g(Date.now()),v(0);try{let e=new URLSearchParams({stream:`1`});d&&e.set(`from_snapshot`,d);let t=await fetch(`${FF}/builds?${e.toString()}`,{method:`POST`,credentials:`include`,headers:{"Content-Type":`application/json`},body:`{}`});if(!t.ok||!t.body)throw Error(`build request failed: HTTP ${t.status}${t.statusText?` `+t.statusText:``}`);let n=t.body.getReader(),r=new TextDecoder,a=``,o=null;for(;;){let{value:e,done:t}=await n.read();if(t)break;a+=r.decode(e,{stream:!0});let i;for(;(i=a.indexOf(`
28
28
  `))>=0;){let e=a.slice(0,i).trim();if(a=a.slice(i+1),!e)continue;let t;try{t=JSON.parse(e)}catch{m(t=>[...t,e]);continue}if(t.type===`log`&&typeof t.line==`string`){let e=t.line;m(t=>[...t,e])}else if(t.type===`start`)m(e=>[...e,`[stream] build ${String(t.buildId??`?`)} started (image=${String(t.image??`?`)})`]);else if(t.type!==`done`&&t.type===`error`){o=String(t.message??`build failed`);let e=typeof t.stderr==`string`&&t.stderr.length>0?`\n${t.stderr}`:``;m(t=>[...t,`[error] ${o}${e}`])}}}let s=a.trim();if(s)try{let e=JSON.parse(s);e.type===`log`&&typeof e.line==`string`&&m(t=>[...t,e.line])}catch{}o&&i(o),await b()}catch(e){i(e instanceof Error?e.message:String(e))}finally{c(!1),g(null)}}async function C(t,n,r){u(t),i(null);try{let a=new URLSearchParams({build_id:t,role:n});r&&a.set(`reset`,`1`);let o=await IF(`${FF}/builds/use?${a.toString()}`,{method:`POST`});r&&typeof o.reset==`object`&&o.reset!==null&&`failed`in o.reset&&i(`Switched, but reset failed: ${o.reset.failed}`),await b(),e()}catch(e){i(e instanceof Error?e.message:String(e))}finally{u(null)}}return(0,J.jsxs)(`section`,{children:[(0,J.jsx)(ZF,{title:`Builds`,description:(()=>{let e=t?.pointers?.browser,n=t?.pointers?.task;if(!e&&!n)return`No build selected — the runner uses the configured default image.`;if(e&&n&&e.snapshotName===n.snapshotName)return`In use (Browser + Task): ${e.snapshotName}`;let r=[];return e&&r.push(`Browser: ${e.snapshotName}`),n&&r.push(`Task: ${n.snapshotName}`),r.join(` · `)})(),actions:(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`button`,{type:`button`,onClick:()=>void b(),disabled:a,className:`btn-ghost flex items-center gap-1.5 px-2 py-1 text-[11px] text-gray-400`,children:[(0,J.jsx)(ZT,{size:12,className:a?`animate-spin`:``}),`Refresh`]}),(0,J.jsxs)(`select`,{value:d,onChange:e=>f(e.target.value),disabled:s,className:`rounded border border-gray-700 bg-gray-900 px-2 py-1 text-[11px] text-gray-200 disabled:cursor-not-allowed disabled:opacity-50`,title:`Optional: layer this build on top of an existing snapshot. Leave as 'fresh image' to use the Sandboxfile's image: line.`,children:[(0,J.jsx)(`option`,{value:``,children:`based on: fresh image`}),(t?.builds??[]).map(e=>(0,J.jsxs)(`option`,{value:e.snapshotName,children:[`based on: `,e.buildId]},e.snapshotName))]}),(0,J.jsxs)(`button`,{type:`button`,onClick:()=>void x(),disabled:s,className:`flex items-center gap-1.5 rounded-md bg-emerald-600 px-3 py-1.5 text-[11px] font-medium text-white hover:bg-emerald-500 disabled:cursor-not-allowed disabled:bg-gray-800 disabled:text-gray-500`,title:d?`Build on top of ${d}`:`Run apt/pip/npm/exec from your saved Sandboxfile and capture a snapshot`,children:[s?(0,J.jsx)(db,{size:12,className:`animate-spin`}):(0,J.jsx)(T_,{size:12}),`Build`]})]})}),r&&(0,J.jsx)(QF,{kind:`error`,text:r}),s&&p.length===0&&(0,J.jsx)(QF,{kind:`info`,text:`Building… this typically takes 10-30s for a slim base image plus a few apt/pip layers. `+(_>0?`(${(_/1e3).toFixed(0)}s elapsed)`:``)}),(s||p.length>0)&&(0,J.jsxs)(`div`,{className:`mb-3 overflow-hidden rounded-md border border-gray-800 bg-gray-950`,children:[(0,J.jsx)(`div`,{className:`flex items-center justify-between border-b border-gray-800 px-3 py-1.5`,children:(0,J.jsxs)(`div`,{className:`flex items-center gap-2 text-[11px] text-gray-300`,children:[s?(0,J.jsx)(db,{size:11,className:`animate-spin text-emerald-400`}):(0,J.jsx)(W,{size:11,className:`text-emerald-400`}),(0,J.jsx)(`span`,{children:s?`Build in progress`:`Last build log`}),s&&_>0&&(0,J.jsxs)(`span`,{className:`text-[10px] text-gray-500`,children:[(_/1e3).toFixed(0),`s`]}),(0,J.jsxs)(`span`,{className:`text-[10px] text-gray-600`,children:[p.length,` line`,p.length===1?``:`s`]})]})}),(0,J.jsx)(`pre`,{ref:y,className:`max-h-72 overflow-auto whitespace-pre-wrap px-3 py-2 font-mono text-[11px] leading-relaxed text-gray-300`,children:p.length>0?p.join(`
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Tianshu — AI agent platform</title>
7
7
  <meta name="description" content="An open AI agent platform with a sidecar browser. Built in public." />
8
- <script type="module" crossorigin src="/assets/index-BMFcYw7S.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CEIw68Su.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-xU4lYYqY.css">
10
10
  </head>
11
11
  <body class="bg-slate-950 text-slate-100">
@@ -56,7 +56,8 @@
56
56
  { "id": "task_retry_fresh", "module": "TaskRetryFreshTool" },
57
57
  { "id": "task_extend_timeout", "module": "TaskExtendTimeoutTool" },
58
58
  { "id": "task_abort", "module": "TaskAbortTool" },
59
- { "id": "model_list", "module": "ModelListTool" }
59
+ { "id": "model_list", "module": "ModelListTool" },
60
+ { "id": "worker_analytics", "module": "WorkerAnalyticsTool" }
60
61
  ],
61
62
  "skills": [
62
63
  { "id": "workboard-howto", "path": "skills/workboard-howto.md" },