See what a risky command would change before it runs.

English | 中文
A desktop AI agent assistant that stays on call, for macOS and Windows, built on DeepSeek Harness (dsh 0.2.1-alpha.2).
DeepSeek Orb is an unofficial community project and is not affiliated with DeepSeek AI.
A floating ball rests at the edge of your screen. Tell it what you need: quick, visible actions are executed directly in the current app, finished in front of you through Computer Use; long-running complex tasks are dispatched to a background coding session, and the result comes back to the ball when it finishes. The main window keeps the full dsh Web UI — session management, the plugin market, and model settings — so you can still write code, edit files, and run commands in it.
The app collects no telemetry.
The Computer Use Agent inside the floating ball decides how every message is handled; there is no separate task classifier at runtime:
flowchart TD
user["You speak to the floating ball"] --> orb["Computer Use Agent"]
orb -->|"Visible GUI actions"| gui["13 GUI tools<br>executed in the current window"]
orb -->|"Quick lookups: weather, news"| web["web_search / web_fetch<br>answered on the ball"]
orb -->|"Dig through files, produce documents or websites"| bg["code_agent background session<br>queued, returns immediately"]
bg -->|"Completion notice"| orb
web_search / web_fetch.code_agent to a background standard session. Dispatch returns immediately; the ball tells you the background is running and you can keep chatting. When the background session ends and the ball is idle, the completion summary returns to the ball automatically, and the Computer Use Agent decides the next move — keep clicking, dispatch more background work, or wrap up.A background session is the same kind of session you create manually in the main window; it appears in the dsh_orb folder in the main-window sidebar, where you can open, continue, or stop it. Only the ball conversation that started it can continue or stop it. Follow-up changes to the same deliverable go back to the same background session; unrelated new work opens another one. The Stop button on the floating ball only cancels the ball's Computer Use session and never affects a background session.
On startup the ball rests at the right edge of the primary display, slightly below vertical center, always on top. The default model is DeepSeek-V41-Flash at Max thinking.
Closing the main window does not quit the app; only "Quit DeepSeek Orb" in the right-click menu ends the process.
Every conversation on the ball runs Computer Use: the first message automatically attaches a screenshot of the frontmost app's visible windows, and another screenshot follows every action, so the model always sees the latest screen state. Screenshots automatically omit the ball, the expanded panel, the selection toolbar, and the observation border, and the overlay never blocks clicks while an operation runs. The observed window gets a glowing observation border around it, marking what the agent is looking at. Windows starts in 0–1000 millifraction encoding and macOS starts in pixels; switch either way in the ball's right-click menu or in main-window Settings → Floating Ball.
Tool list: click (single/double/right click, hold modifiers), input_text, scroll, hotkey, long_press, drag, wait, long_wait, screenshot (saved to the Desktop and copied to the clipboard), open_in_browser, open_in_finder, list_apps, open_app.
| Platform | Floating ball and Computer Use | Installers |
|---|---|---|
| macOS (Apple Silicon / Intel) | Fully supported; Screen Recording + Accessibility permissions required | Signed DMG / ZIP; local unsigned preview build |
| Windows x64 | Fully supported; administrator windows refuse to be operated | NSIS installer (.exe); local unsigned build |
| Linux | No floating ball; the main window works, settings controls are disabled | Not a release target |
^22.19.0 || >=24.0.0, pnpm 11.28.5.export DEEPSEEK_API_KEY=… before starting. Keys live in ~/.dsh/.credentials.yaml, shared with the dsh CLI.dsh_orb workspace, no configuration needed.<a id="run-from-source"></a>
git clone https://github.com/mini-yifan/deepseek-harness-orb.git
cd deepseek-harness-orb
pnpm install
pnpm run dev:desktop
dev:desktop builds the Host, clients, Web frontend, and the Electron shell, then launches the unpackaged app; once everything is built, pnpm run start:desktop skips the build. The main window shows a splash screen first, loads the Web UI when ready, and the floating ball appears at the screen edge.
| Target | Command | Notes |
|---|---|---|
| macOS (Apple Silicon) | pnpm run package:desktop:mac:arm64 | Signed release flow in MAC-RELEASE-PACK.md; unsigned preview build in UNSIGNED-MAC-PACK.md |
| Windows x64 (unsigned) | pnpm run package:desktop:win:x64:unsigned | Environment variables and tool cache in UNSIGNED-WIN-PACK.md; SmartScreen will block it — choose "Run anyway" |
Packaging details and platform requirements: apps/desktop/README.md.
This repository is an unofficial derivative of deepseek-ai/deepseek-harness, built on dsh 0.2.1-alpha.2 and not affiliated with DeepSeek AI. The main branch carries the DeepSeek Orb product and merges upstream updates continuously. dsh is DeepSeek AI's open-source everything-is-a-plugin agent harness; this product's sessions, plugins, tools, and Web UI are all driven by it. To go deeper:
MIT, inherited from upstream DeepSeek Harness; third-party dependency licenses are disclosed in THIRD_PARTY_NOTICES.md.
hooks/blast-radius.mjs 128 lines1// Blast Radius, from "Getting started with Claude Code mods"
2// (https://claude.dev/blog/getting-started-with-claude-code-mods/, Anthropic, 2026-10-01).
3// The tool.call hook adds a waitArgv option for DSH's portable timer; the rest of the module
4// (classify, measure, the pane and band trees) is completed to the post's
5// description of the mod, which is marked where it starts.
6
7// Blast Radius: see what a risky command would change before it runs.
8
9// —— completed to the post's description (not in the published excerpt) ——
10
11const RISKS = [
12 { pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)\b/, kind: "delete", what: "delete files recursively" },
13 { pattern: /\bgit\s+reset\s+--hard\b/, kind: "reset", what: "discard uncommitted changes" },
14 { pattern: /\bgit\s+clean\b/, kind: "clean", what: "delete untracked files" },
15 { pattern: /\bgit\s+push\b[^\n]*\s(--force|-f)\b/, kind: "force-push", what: "rewrite a remote branch" },
16 { pattern: /\b(migrate|manage\.py\s+migrate|prisma\s+migrate|rails\s+db:migrate)\b/, kind: "migrate", what: "change the database schema" },
17];
18
19// What the hook is holding: one command at a time, until a button decides.
20let held = null;
21
22export function register(on, { waitArgv = ["sleep", "0.25"] } = {}) {
23 on("tool.call", { tool: "Bash" }, async ($, e, next) => {
24 const risk = classify(String(e.command ?? ""));
25 if (risk === null) return next(e); // everything else runs as normal
26
27 const report = await measure($, risk, await $.session.cwd()); // git status, git clean -n, du, ...
28 held = { command: e.command, risk, report, decision: null };
29 const opened = await $.ui.open({ id: "blast-radius", title: "Blast Radius", focus: true });
30 if (!opened.isPlaced) held.where = "band"; // too narrow for a pane: draw above the prompt
31
32 while (held.decision === null && !next.signal.aborted) {
33 await $.process.run(waitArgv); // time inside $ calls doesn't count against the hook's time limit
34 }
35 if (held.decision === "proceed") return next(e); // let it run
36 return { deny: `Blast Radius held this command: the user pressed Cancel. It would have: ${report.summary}.` };
37 });
38
39 // —— completed to the post's description (not in the published excerpt) ——
40
41 on("ui.render", { component: "Pane" }, ($, e, next) => {
42 if (e.requestId !== "blast-radius" || held === null || held.decision !== null) return next(e);
43 return report($, e);
44 });
45
46 on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
47 if (held === null || held.decision !== null || held.where !== "band") return next(e);
48 return report($, e);
49 });
50}
51
52function classify(command) {
53 const risk = RISKS.find((r) => r.pattern.test(command));
54 return risk ? { kind: risk.kind, what: risk.what, command } : null;
55}
56
57async function measure($, risk, cwd) {
58 const lines = [];
59 let summary = `${risk.what} in ${cwd}`;
60 if (risk.kind === "delete") {
61 const target = risk.command.match(/\brm\s+-\S+\s+(\S+)/)?.[1];
62 const du = await run($, ["du", "-sh", target ?? "."]);
63 const size = du.stdout.trim().split(/\s+/)[0] ?? "?";
64 const count = await run($, ["find", target ?? ".", "-type", "f"]);
65 const files = count.stdout.split("\n").filter(Boolean).length;
66 summary = `delete ${files} file(s) (${size}) under ${target ?? cwd}`;
67 lines.push(...count.stdout.split("\n").filter(Boolean).slice(0, 9));
68 } else if (risk.kind === "reset") {
69 const status = await run($, ["git", "status", "--porcelain"]);
70 const changed = status.stdout.split("\n").filter(Boolean);
71 summary = `discard uncommitted changes to ${changed.length} file(s)`;
72 lines.push(...changed.slice(0, 9));
73 } else if (risk.kind === "clean") {
74 const dry = await run($, ["git", "clean", "-n"]);
75 const removed = dry.stdout.split("\n").filter(Boolean);
76 summary = `delete ${removed.length} untracked path(s)`;
77 lines.push(...removed.slice(0, 9));
78 } else if (risk.kind === "force-push") {
79 const ahead = await run($, ["git", "log", "--oneline", "HEAD..origin/main"]);
80 const commits = ahead.stdout.split("\n").filter(Boolean);
81 summary = `rewrite the remote branch, dropping ${commits.length} commit(s) not on this branch`;
82 lines.push(...commits.slice(0, 9));
83 } else if (risk.kind === "migrate") {
84 const pending = await run($, ["sh", "-c", "python manage.py showmigrations --plan 2>/dev/null | grep '\\[ \\]' || true"]);
85 const migrations = pending.stdout.split("\n").filter(Boolean);
86 summary = `apply ${migrations.length} pending migration(s)`;
87 lines.push(...migrations.slice(0, 9));
88 }
89 return { summary, lines };
90}
91
92// A dry run that fails (no git, no python, no such path) still lets the user decide.
93async function run($, argv) {
94 try {
95 return await $.process.run(argv);
96 } catch {
97 return { exitCode: 1, stdout: "", stderr: "" };
98 }
99}
100
101function decide(decision) {
102 if (held !== null) held.decision = decision;
103}
104
105function report($, e) {
106 const { Box, Text, Button } = $.ui.resolve(e);
107 const detail = held.report.lines.map((line) => Text({ dimColor: true, children: ` ${line}` }));
108 return Box({
109 flexDirection: "column",
110 border: true,
111 borderColor: "yellow",
112 paddingX: 1,
113 children: [
114 Text({ color: "yellow", bold: true, children: `Blast Radius: ${held.command}` }),
115 Text({ children: `Would ${held.report.summary}.` }),
116 ...detail,
117 Box({
118 flexDirection: "row",
119 gap: 2,
120 children: [
121 Button({ label: "Proceed", hotkey: "1", onPress: () => decide("proceed") }),
122 Button({ label: "Cancel", hotkey: "2", onPress: () => decide("cancel") }),
123 ],
124 }),
125 ],
126 });
127}
128