Update version to 0.1.10
Build / Build & push landing (push) Successful in 14s
Build / Deploy to prod (push) Successful in 7s
Build / Notify Max (push) Successful in 2s

Add deepseek to resume commands

Rename app to spaceshell

Add SurfacePicker component for preset panel configuration

Extract agent selection logic to shared agents.ts

Update landing
This commit is contained in:
2026-06-15 17:25:53 +07:00
parent 333b051e9d
commit 2ee2aaaffb
12 changed files with 151 additions and 46 deletions
+1 -1
View File
@@ -3440,7 +3440,7 @@ dependencies = [
[[package]]
name = "spacesh-proto"
version = "0.1.7"
version = "0.1.10"
dependencies = [
"bytes",
"serde",
+3 -3
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "spacesh",
"version": "0.1.7",
"productName": "spaceshell",
"version": "0.1.10",
"identifier": "xyz.spacesh.app",
"build": {
"frontendDist": "../dist",
@@ -12,7 +12,7 @@
"app": {
"windows": [
{
"title": "spacesh",
"title": "spaceshell",
"width": 1100,
"height": 720
}
+23 -1
View File
@@ -4,6 +4,8 @@ import { Sidebar } from "./Sidebar";
import { TopBar } from "./TopBar";
import { CenterToolbar } from "./CenterToolbar";
import { Wizard } from "./Wizard";
import { SurfacePicker } from "./SurfacePicker";
import { PRESETS } from "./PresetPicker";
import { ConfirmDelete } from "./ConfirmDelete";
import { Settings } from "./Settings";
import { EventCenter } from "./EventCenter";
@@ -31,6 +33,8 @@ export function App() {
const [states, setStates] = useState<Record<string, SurfaceState>>({});
const [events, setEvents] = useState<EventRecord[]>([]);
const [wizard, setWizard] = useState(false);
// Pending additive preset awaiting the per-panel "what to open" choice.
const [pendingPreset, setPendingPreset] = useState<{ id: string; delta: number; base: number } | null>(null);
const [deleteTarget, setDeleteTarget] = useState<WorkspaceView | null>(null);
const [settingsOpen, setSettingsOpen] = useState(false);
const [eventsOpen, setEventsOpen] = useState(() => loadFlag("spacesh.eventsOpen", true));
@@ -183,7 +187,13 @@ export function App() {
<Sidebar railMode={!sidebarOpen} groups={groups} workspaces={workspaces} activeId={activeId} onSelect={selectWorkspace} onNew={() => setWizard(true)} onDelete={setDeleteTarget} health={health} connected={connected} />
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
{active && (
<CenterToolbar selected="" paneCount={leaves.length} onSelect={(p) => { if (active) void applyPreset(active.id, p, []); }} onOpenSearch={() => { if (effectiveFocus) { setSearchSurfaceId(effectiveFocus); setSearchNonce((n) => n + 1); } }} />
<CenterToolbar selected="" paneCount={leaves.length} onSelect={(p) => {
if (!active) return;
const target = PRESETS.find((x) => x.id === p)?.slots ?? leaves.length;
const delta = target - leaves.length;
if (delta <= 0) { void applyPreset(active.id, p, []); return; } // reshape only — no new panels
setPendingPreset({ id: p, delta, base: leaves.length });
}} onOpenSearch={() => { if (effectiveFocus) { setSearchSurfaceId(effectiveFocus); setSearchNonce((n) => n + 1); } }} />
)}
<div style={{ flex: 1, minHeight: 0, position: "relative" }}>
{active
@@ -202,6 +212,18 @@ export function App() {
</div>
{settingsOpen && config && <Settings config={config} health={health} onClose={() => setSettingsOpen(false)} onReload={() => { void loadHealth(); void refresh(); }} />}
{wizard && <Wizard onDone={(id) => { setWizard(false); setActiveId(id); void refresh(); }} onCancel={() => setWizard(false)} />}
{pendingPreset && active && (
<SurfacePicker
count={pendingPreset.delta}
onCancel={() => setPendingPreset(null)}
onConfirm={(specs) => {
const padded = [...Array(pendingPreset.base).fill({}), ...specs]; // align to daemon's slots.get(existing.len()+j)
const wsId = active.id;
setPendingPreset(null);
void applyPreset(wsId, pendingPreset.id, padded).then(() => void refresh());
}}
/>
)}
{deleteTarget && (
<ConfirmDelete
name={deleteTarget.name}
+70
View File
@@ -0,0 +1,70 @@
import { useEffect, useState } from "react";
import { whichAgents } from "./socketBridge";
import { KNOWN_AGENTS, SHELL, CUSTOM, agentLabel, specForChoice } from "./agents";
type SlotSpec = { command?: string; args?: string[] };
/**
* Asks what to open in each new panel before a preset spawns it: Terminal
* (shell), one of the installed CLIs (claude/codex/gemini/deepseek), or a
* custom command. `count` is the number of new panels the preset will add.
*/
export function SurfacePicker({ count, onConfirm, onCancel }: { count: number; onConfirm: (specs: SlotSpec[]) => void; onCancel: () => void }) {
const [installed, setInstalled] = useState<string[]>([]);
const [choices, setChoices] = useState<string[]>([]);
const [customCmds, setCustomCmds] = useState<string[]>([]);
const choiceList = [SHELL, ...installed, CUSTOM];
useEffect(() => { void whichAgents(KNOWN_AGENTS).then(setInstalled).catch(() => {}); }, []);
function confirm() {
const specs = Array.from({ length: count }, (_, i) => specForChoice(choices[i] ?? SHELL, customCmds[i] ?? ""));
onConfirm(specs);
}
function onKeyDown(e: React.KeyboardEvent) {
e.stopPropagation();
if (e.key === "Escape") { e.preventDefault(); onCancel(); }
else if (e.key === "Enter" && (e.target as HTMLElement).tagName !== "SELECT") { e.preventDefault(); confirm(); }
}
return (
<div
onMouseDown={onCancel}
style={{ position: "fixed", inset: 0, zIndex: 2000, background: "#000A", display: "flex", alignItems: "center", justifyContent: "center" }}
>
<div
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={onKeyDown}
style={{ width: 420, background: "#0E1116", border: "1px solid #323C49", borderRadius: 14, padding: 24, color: "#E6EDF3" }}
>
<div style={{ fontWeight: 700, fontSize: 16, marginBottom: 4 }}>{count > 1 ? `Open ${count} new panels` : "Open new panel"}</div>
<div style={{ fontSize: 12, color: "#8B97A6", marginBottom: 16 }}>Choose what to run in each new panel.</div>
<div style={{ display: "grid", gridTemplateColumns: count > 1 ? "1fr 1fr" : "1fr", gap: 8, marginBottom: 20 }}>
{Array.from({ length: count }, (_, i) => {
const val = choices[i] ?? SHELL;
return (
<div key={i} style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<select value={val} onChange={(e) => setChoices((c) => { const n = [...c]; n[i] = e.target.value; return n; })}
style={{ padding: 8, background: "#1A2029", color: "#E6EDF3", border: "1px solid #323C49", borderRadius: 6 }}>
{choiceList.map((c) => <option key={c} value={c}>{agentLabel(c)}</option>)}
</select>
{val === CUSTOM && (
<input value={customCmds[i] ?? ""} placeholder="e.g. npm run dev" autoFocus
onChange={(e) => setCustomCmds((c) => { const n = [...c]; n[i] = e.target.value; return n; })}
style={{ padding: 8, background: "#0A0D12", color: "#E6EDF3", border: "1px solid #4C8DFF", borderRadius: 6, fontFamily: "monospace", fontSize: 12 }} />
)}
</div>
);
})}
</div>
<div style={{ display: "flex", justifyContent: "flex-end", gap: 10 }}>
<button onClick={onCancel} style={{ padding: "8px 16px" }}>Cancel</button>
<button onClick={confirm} style={{ padding: "8px 16px", background: "#4C8DFF", color: "#0A0D12", border: "none", borderRadius: 8, fontWeight: 700 }}>
Open
</button>
</div>
</div>
</div>
);
}
+1 -1
View File
@@ -138,7 +138,7 @@ export function TopBar({
<div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
<FolderGit2 size={15} color={COLORS.textSecondary} />
<span style={{ fontFamily: FONT.ui, fontSize: 13, fontWeight: 600, color: COLORS.textPrimary, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{active?.name ?? "spacesh"}
{active?.name ?? "spaceshell"}
</span>
{active && (
<>
+5 -16
View File
@@ -1,10 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { PresetPicker, PRESETS } from "./PresetPicker";
import { openWorkspace, applyPreset, whichAgents } from "./socketBridge";
// Agents we know about; only the installed ones are offered (probed via whichAgents).
const KNOWN_AGENTS = ["claude", "codex", "gemini"];
const CUSTOM = "custom…";
import { KNOWN_AGENTS, SHELL, CUSTOM, agentLabel, specForChoice } from "./agents";
export function Wizard({ onDone, onCancel }: { onDone: (workspaceId: string) => void; onCancel: () => void }) {
const [path, setPath] = useState(".");
@@ -16,7 +13,7 @@ export function Wizard({ onDone, onCancel }: { onDone: (workspaceId: string) =>
const [error, setError] = useState<string | null>(null);
const pathRef = useRef<HTMLInputElement>(null);
const slots = PRESETS.find((p) => p.id === preset)?.slots ?? 1;
const agentChoices = ["shell", ...installed, CUSTOM];
const agentChoices = [SHELL, ...installed, CUSTOM];
// Grab focus on open — otherwise keystrokes leak to the xterm panel behind us
// (its helper textarea sits at z-index 1000 and keeps the live focus).
@@ -34,15 +31,7 @@ export function Wizard({ onDone, onCancel }: { onDone: (workspaceId: string) =>
setError(null);
try {
const ws = await openWorkspace(path);
const slotSpecs = Array.from({ length: slots }, (_, i) => {
const a = agents[i] ?? "shell";
if (a === "shell") return {};
if (a === CUSTOM) {
const parts = (customCmds[i] ?? "").trim().split(/\s+/).filter(Boolean);
return parts.length ? { command: parts[0], args: parts.slice(1) } : {};
}
return { command: a };
});
const slotSpecs = Array.from({ length: slots }, (_, i) => specForChoice(agents[i] ?? SHELL, customCmds[i] ?? ""));
await applyPreset(ws, preset, slotSpecs);
onDone(ws);
} catch (e) {
@@ -76,12 +65,12 @@ export function Wizard({ onDone, onCancel }: { onDone: (workspaceId: string) =>
<label style={{ fontSize: 12, color: "#8B97A6" }}>Agents</label>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, margin: "8px 0 20px" }}>
{Array.from({ length: slots }, (_, i) => {
const val = agents[i] ?? "shell";
const val = agents[i] ?? SHELL;
return (
<div key={i} style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<select value={val} onChange={(e) => setAgents((a) => { const n = [...a]; n[i] = e.target.value; return n; })}
style={{ padding: 8, background: "#1A2029", color: "#E6EDF3", border: "1px solid #323C49", borderRadius: 6 }}>
{agentChoices.map((c) => <option key={c} value={c}>{c}</option>)}
{agentChoices.map((c) => <option key={c} value={c}>{agentLabel(c)}</option>)}
</select>
{val === CUSTOM && (
<input value={customCmds[i] ?? ""} placeholder="e.g. npm run dev"
+22
View File
@@ -0,0 +1,22 @@
// Launchable agents/CLIs offered when opening a new panel. Only the installed
// ones are surfaced (probed via whichAgents); "shell" and "custom…" are always
// available. Keep this list as the single source of truth — Wizard and
// SurfacePicker both consume it.
export const KNOWN_AGENTS = ["claude", "codex", "gemini", "deepseek", "opencode"];
export const SHELL = "shell";
export const CUSTOM = "custom…";
/** Human label for an agent choice (the shell is presented as "Terminal"). */
export function agentLabel(choice: string): string {
return choice === SHELL ? "Terminal" : choice;
}
/** Map a picker choice (+ optional custom command line) to an applyPreset slot spec. */
export function specForChoice(choice: string, custom: string): { command?: string; args?: string[] } {
if (choice === SHELL) return {};
if (choice === CUSTOM) {
const parts = (custom ?? "").trim().split(/\s+/).filter(Boolean);
return parts.length ? { command: parts[0], args: parts.slice(1) } : {};
}
return { command: choice };
}