Update version to 0.1.10
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:
Generated
+1
-1
@@ -3440,7 +3440,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spacesh-proto"
|
||||
version = "0.1.7"
|
||||
version = "0.1.10"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"serde",
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user