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
+5
-5
@@ -869,7 +869,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacesh-cli"
|
name = "spacesh-cli"
|
||||||
version = "0.1.7"
|
version = "0.1.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -881,7 +881,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacesh-core"
|
name = "spacesh-core"
|
||||||
version = "0.1.7"
|
version = "0.1.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alacritty_terminal",
|
"alacritty_terminal",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -891,7 +891,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacesh-proto"
|
name = "spacesh-proto"
|
||||||
version = "0.1.7"
|
version = "0.1.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -903,7 +903,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacesh-pty"
|
name = "spacesh-pty"
|
||||||
version = "0.1.7"
|
version = "0.1.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -913,7 +913,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spaceshd"
|
name = "spaceshd"
|
||||||
version = "0.1.7"
|
version = "0.1.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ members = [
|
|||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
version = "0.1.7"
|
version = "0.1.10"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ In `crates/spaceshd/src/config.rs`, add the struct and a default table, and exte
|
|||||||
const DEFAULT_RESUME: &[(&str, &[&str])] = &[
|
const DEFAULT_RESUME: &[(&str, &[&str])] = &[
|
||||||
("claude", &["--continue"]),
|
("claude", &["--continue"]),
|
||||||
("codex", &["resume"]),
|
("codex", &["resume"]),
|
||||||
|
("deepseek", &["resume"]),
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ NATIVE_DMG_DIR := $(APP_DIR)/src-tauri/target/release/bundle/dmg
|
|||||||
NATIVE_TRIPLE := $(shell rustc -vV 2>/dev/null | awk '/^host:/{print $$2}')
|
NATIVE_TRIPLE := $(shell rustc -vV 2>/dev/null | awk '/^host:/{print $$2}')
|
||||||
SIDECAR_DIR := $(APP_DIR)/src-tauri/bin
|
SIDECAR_DIR := $(APP_DIR)/src-tauri/bin
|
||||||
BUNDLE_CONFIG := src-tauri/tauri.bundle.conf.json
|
BUNDLE_CONFIG := src-tauri/tauri.bundle.conf.json
|
||||||
APP_BUNDLE := $(APP_DIR)/src-tauri/target/$(TAURI_TARGET)/release/bundle/macos/spacesh.app
|
APP_BUNDLE := $(APP_DIR)/src-tauri/target/$(TAURI_TARGET)/release/bundle/macos/spaceshell.app
|
||||||
NATIVE_APP_BUNDLE := $(APP_DIR)/src-tauri/target/release/bundle/macos/spacesh.app
|
NATIVE_APP_BUNDLE := $(APP_DIR)/src-tauri/target/release/bundle/macos/spaceshell.app
|
||||||
APP_VERSION := $(shell node -p "require('./$(APP_DIR)/src-tauri/tauri.conf.json').version" 2>/dev/null || echo 0.0.0)
|
APP_VERSION := $(shell node -p "require('./$(APP_DIR)/src-tauri/tauri.conf.json').version" 2>/dev/null || echo 0.0.0)
|
||||||
|
|
||||||
LANDING_IMAGE := spacesh-landing
|
LANDING_IMAGE := spacesh-landing
|
||||||
@@ -19,7 +19,7 @@ REPO ?= spacesh
|
|||||||
|
|
||||||
# ---- Gitea generic package registry (versioned .dmg downloads) ----
|
# ---- Gitea generic package registry (versioned .dmg downloads) ----
|
||||||
GITEA_URL ?= https://git.realmanual.ru
|
GITEA_URL ?= https://git.realmanual.ru
|
||||||
GITEA_OWNER ?= realmanual
|
GITEA_OWNER ?= pub
|
||||||
GITEA_PKG ?= spacesh
|
GITEA_PKG ?= spacesh
|
||||||
GITEA_TOKEN ?= # token with package:write; pass via env/CLI, never commit
|
GITEA_TOKEN ?= # token with package:write; pass via env/CLI, never commit
|
||||||
|
|
||||||
@@ -99,16 +99,16 @@ kill-daemon: ## stop a running spaceshd so a freshly-built one takes over
|
|||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: kill-daemon ## install the native .app to /Applications, restart daemon, clear quarantine
|
install: kill-daemon ## install the native .app to /Applications, restart daemon, clear quarantine
|
||||||
rm -rf /Applications/spacesh.app
|
rm -rf /Applications/spacesh.app /Applications/spaceshell.app # drop the pre-rename app too
|
||||||
cp -R "$(NATIVE_APP_BUNDLE)" /Applications/
|
cp -R "$(NATIVE_APP_BUNDLE)" /Applications/
|
||||||
xattr -dr com.apple.quarantine /Applications/spacesh.app
|
xattr -dr com.apple.quarantine /Applications/spaceshell.app
|
||||||
@echo "Installed (native). Quit & relaunch spacesh; the bundled daemon restarts."
|
@echo "Installed (native). Quit & relaunch spaceshell; the bundled daemon restarts."
|
||||||
|
|
||||||
.PHONY: install-universal
|
.PHONY: install-universal
|
||||||
install-universal: kill-daemon ## install the universal .app to /Applications
|
install-universal: kill-daemon ## install the universal .app to /Applications
|
||||||
rm -rf /Applications/spacesh.app
|
rm -rf /Applications/spacesh.app /Applications/spaceshell.app
|
||||||
cp -R "$(APP_BUNDLE)" /Applications/
|
cp -R "$(APP_BUNDLE)" /Applications/
|
||||||
xattr -dr com.apple.quarantine /Applications/spacesh.app
|
xattr -dr com.apple.quarantine /Applications/spaceshell.app
|
||||||
|
|
||||||
.PHONY: reinstall
|
.PHONY: reinstall
|
||||||
reinstall: app-bundle install ## fast self-update: build .app (no dmg), reinstall, restart daemon
|
reinstall: app-bundle install ## fast self-update: build .app (no dmg), reinstall, restart daemon
|
||||||
@@ -162,10 +162,10 @@ _publish-dmg:
|
|||||||
VER=$$(node -p "require('./$(APP_DIR)/src-tauri/tauri.conf.json').version"); \
|
VER=$$(node -p "require('./$(APP_DIR)/src-tauri/tauri.conf.json').version"); \
|
||||||
DMG=$$(ls -t $(DMG_DIR)/*.dmg 2>/dev/null | head -1); \
|
DMG=$$(ls -t $(DMG_DIR)/*.dmg 2>/dev/null | head -1); \
|
||||||
if [ -z "$$DMG" ]; then echo "no .dmg in $(DMG_DIR) — run make dmg first"; exit 1; fi; \
|
if [ -z "$$DMG" ]; then echo "no .dmg in $(DMG_DIR) — run make dmg first"; exit 1; fi; \
|
||||||
URL="$(GITEA_URL)/api/packages/$(GITEA_OWNER)/generic/$(GITEA_PKG)/$$VER/spacesh-$$VER.dmg"; \
|
URL="$(GITEA_URL)/api/packages/$(GITEA_OWNER)/generic/$(GITEA_PKG)/$$VER/spaceshell-$$VER.dmg"; \
|
||||||
echo "Publishing $$DMG → $$URL"; \
|
echo "Publishing $$DMG → $$URL"; \
|
||||||
curl --fail-with-body -sS -H "Authorization: token $(GITEA_TOKEN)" --upload-file "$$DMG" "$$URL" && \
|
curl --fail-with-body -sS -H "Authorization: token $(GITEA_TOKEN)" --upload-file "$$DMG" "$$URL" && \
|
||||||
echo "Published spacesh-$$VER.dmg to Gitea Packages ($(GITEA_OWNER)/$(GITEA_PKG)@$$VER)"
|
echo "Published spaceshell-$$VER.dmg to Gitea Packages ($(GITEA_OWNER)/$(GITEA_PKG)@$$VER)"
|
||||||
|
|
||||||
.PHONY: deploy-stack
|
.PHONY: deploy-stack
|
||||||
deploy-stack: ## sync compose+proxy.conf to prod and pull/up (manual; CI does this on push)
|
deploy-stack: ## sync compose+proxy.conf to prod and pull/up (manual; CI does this on push)
|
||||||
|
|||||||
Generated
+1
-1
@@ -3440,7 +3440,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spacesh-proto"
|
name = "spacesh-proto"
|
||||||
version = "0.1.7"
|
version = "0.1.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "spacesh",
|
"productName": "spaceshell",
|
||||||
"version": "0.1.7",
|
"version": "0.1.10",
|
||||||
"identifier": "xyz.spacesh.app",
|
"identifier": "xyz.spacesh.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "spacesh",
|
"title": "spaceshell",
|
||||||
"width": 1100,
|
"width": 1100,
|
||||||
"height": 720
|
"height": 720
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-1
@@ -4,6 +4,8 @@ import { Sidebar } from "./Sidebar";
|
|||||||
import { TopBar } from "./TopBar";
|
import { TopBar } from "./TopBar";
|
||||||
import { CenterToolbar } from "./CenterToolbar";
|
import { CenterToolbar } from "./CenterToolbar";
|
||||||
import { Wizard } from "./Wizard";
|
import { Wizard } from "./Wizard";
|
||||||
|
import { SurfacePicker } from "./SurfacePicker";
|
||||||
|
import { PRESETS } from "./PresetPicker";
|
||||||
import { ConfirmDelete } from "./ConfirmDelete";
|
import { ConfirmDelete } from "./ConfirmDelete";
|
||||||
import { Settings } from "./Settings";
|
import { Settings } from "./Settings";
|
||||||
import { EventCenter } from "./EventCenter";
|
import { EventCenter } from "./EventCenter";
|
||||||
@@ -31,6 +33,8 @@ export function App() {
|
|||||||
const [states, setStates] = useState<Record<string, SurfaceState>>({});
|
const [states, setStates] = useState<Record<string, SurfaceState>>({});
|
||||||
const [events, setEvents] = useState<EventRecord[]>([]);
|
const [events, setEvents] = useState<EventRecord[]>([]);
|
||||||
const [wizard, setWizard] = useState(false);
|
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 [deleteTarget, setDeleteTarget] = useState<WorkspaceView | null>(null);
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [eventsOpen, setEventsOpen] = useState(() => loadFlag("spacesh.eventsOpen", true));
|
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} />
|
<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 }}>
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||||
{active && (
|
{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" }}>
|
<div style={{ flex: 1, minHeight: 0, position: "relative" }}>
|
||||||
{active
|
{active
|
||||||
@@ -202,6 +212,18 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
{settingsOpen && config && <Settings config={config} health={health} onClose={() => setSettingsOpen(false)} onReload={() => { void loadHealth(); void refresh(); }} />}
|
{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)} />}
|
{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 && (
|
{deleteTarget && (
|
||||||
<ConfirmDelete
|
<ConfirmDelete
|
||||||
name={deleteTarget.name}
|
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 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
|
||||||
<FolderGit2 size={15} color={COLORS.textSecondary} />
|
<FolderGit2 size={15} color={COLORS.textSecondary} />
|
||||||
<span style={{ fontFamily: FONT.ui, fontSize: 13, fontWeight: 600, color: COLORS.textPrimary, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
<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>
|
</span>
|
||||||
{active && (
|
{active && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
+5
-16
@@ -1,10 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { PresetPicker, PRESETS } from "./PresetPicker";
|
import { PresetPicker, PRESETS } from "./PresetPicker";
|
||||||
import { openWorkspace, applyPreset, whichAgents } from "./socketBridge";
|
import { openWorkspace, applyPreset, whichAgents } from "./socketBridge";
|
||||||
|
import { KNOWN_AGENTS, SHELL, CUSTOM, agentLabel, specForChoice } from "./agents";
|
||||||
// Agents we know about; only the installed ones are offered (probed via whichAgents).
|
|
||||||
const KNOWN_AGENTS = ["claude", "codex", "gemini"];
|
|
||||||
const CUSTOM = "custom…";
|
|
||||||
|
|
||||||
export function Wizard({ onDone, onCancel }: { onDone: (workspaceId: string) => void; onCancel: () => void }) {
|
export function Wizard({ onDone, onCancel }: { onDone: (workspaceId: string) => void; onCancel: () => void }) {
|
||||||
const [path, setPath] = useState(".");
|
const [path, setPath] = useState(".");
|
||||||
@@ -16,7 +13,7 @@ export function Wizard({ onDone, onCancel }: { onDone: (workspaceId: string) =>
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const pathRef = useRef<HTMLInputElement>(null);
|
const pathRef = useRef<HTMLInputElement>(null);
|
||||||
const slots = PRESETS.find((p) => p.id === preset)?.slots ?? 1;
|
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
|
// 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).
|
// (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);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const ws = await openWorkspace(path);
|
const ws = await openWorkspace(path);
|
||||||
const slotSpecs = Array.from({ length: slots }, (_, i) => {
|
const slotSpecs = Array.from({ length: slots }, (_, i) => specForChoice(agents[i] ?? SHELL, customCmds[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 };
|
|
||||||
});
|
|
||||||
await applyPreset(ws, preset, slotSpecs);
|
await applyPreset(ws, preset, slotSpecs);
|
||||||
onDone(ws);
|
onDone(ws);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -76,12 +65,12 @@ export function Wizard({ onDone, onCancel }: { onDone: (workspaceId: string) =>
|
|||||||
<label style={{ fontSize: 12, color: "#8B97A6" }}>Agents</label>
|
<label style={{ fontSize: 12, color: "#8B97A6" }}>Agents</label>
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, margin: "8px 0 20px" }}>
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, margin: "8px 0 20px" }}>
|
||||||
{Array.from({ length: slots }, (_, i) => {
|
{Array.from({ length: slots }, (_, i) => {
|
||||||
const val = agents[i] ?? "shell";
|
const val = agents[i] ?? SHELL;
|
||||||
return (
|
return (
|
||||||
<div key={i} style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
<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; })}
|
<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 }}>
|
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>
|
</select>
|
||||||
{val === CUSTOM && (
|
{val === CUSTOM && (
|
||||||
<input value={customCmds[i] ?? ""} placeholder="e.g. npm run dev"
|
<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 };
|
||||||
|
}
|
||||||
+9
-8
@@ -3,10 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>spacesh — терминал-воркспейс для AI-агентов на macOS</title>
|
<title>spaceshell — терминал-воркспейс для AI-агентов на macOS</title>
|
||||||
<meta name="description" content="Запускай Claude Code, Codex, Gemini и shell параллельно. Фоновый демон держит сессии живыми: закрыл окно — агенты работают. Скачать для macOS.">
|
<meta name="description" content="Запускай Claude Code, Codex, Gemini и shell параллельно. Фоновый демон держит сессии живыми: закрыл окно — агенты работают. Скачать для macOS.">
|
||||||
<link rel="canonical" href="https://spaceshell.ru">
|
<link rel="canonical" href="https://spaceshell.ru">
|
||||||
<meta property="og:title" content="spacesh — терминал-воркспейс для AI-агентов">
|
<meta property="og:title" content="spaceshell — терминал-воркспейс для AI-агентов">
|
||||||
<meta property="og:description" content="Десяток AI-агентов параллельно. Демон держит сессии живыми — закрой окно, агенты работают.">
|
<meta property="og:description" content="Десяток AI-агентов параллельно. Демон держит сессии живыми — закрой окно, агенты работают.">
|
||||||
<meta property="og:url" content="https://spaceshell.ru">
|
<meta property="og:url" content="https://spaceshell.ru">
|
||||||
<meta property="og:image" content="https://spaceshell.ru/og.png">
|
<meta property="og:image" content="https://spaceshell.ru/og.png">
|
||||||
@@ -812,7 +812,7 @@
|
|||||||
<div class="header-inner">
|
<div class="header-inner">
|
||||||
<a href="/" class="logo">
|
<a href="/" class="logo">
|
||||||
<span class="logo-icon">>_</span>
|
<span class="logo-icon">>_</span>
|
||||||
spacesh
|
spaceshell
|
||||||
</a>
|
</a>
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<a href="#features">Возможности</a>
|
<a href="#features">Возможности</a>
|
||||||
@@ -836,7 +836,7 @@
|
|||||||
Гоняй десяток AI-агентов параллельно. <span class="accent">Не теряй ни одного.</span>
|
Гоняй десяток AI-агентов параллельно. <span class="accent">Не теряй ни одного.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="hero-subtitle">
|
<p class="hero-subtitle">
|
||||||
spacesh держит живые сессии Claude Code, Codex, Gemini и shell в фоновом демоне.
|
spaceshell держит живые сессии Claude Code, Codex, Gemini и shell в фоновом демоне.
|
||||||
Закрыл окно, обновил приложение, словил краш — агенты продолжают работать.
|
Закрыл окно, обновил приложение, словил краш — агенты продолжают работать.
|
||||||
</p>
|
</p>
|
||||||
<div class="hero-buttons">
|
<div class="hero-buttons">
|
||||||
@@ -860,7 +860,7 @@
|
|||||||
<span class="terminal-dot"></span>
|
<span class="terminal-dot"></span>
|
||||||
<span class="terminal-dot"></span>
|
<span class="terminal-dot"></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="terminal-title">spacesh — workspace</span>
|
<span class="terminal-title">spaceshell — workspace</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="terminal-grid">
|
<div class="terminal-grid">
|
||||||
<div class="terminal-pane">
|
<div class="terminal-pane">
|
||||||
@@ -928,6 +928,7 @@
|
|||||||
<span class="agent-tag">Codex</span>
|
<span class="agent-tag">Codex</span>
|
||||||
<span class="agent-tag">Gemini</span>
|
<span class="agent-tag">Gemini</span>
|
||||||
<span class="agent-tag">opencode</span>
|
<span class="agent-tag">opencode</span>
|
||||||
|
<span class="agent-tag">deepseek</span>
|
||||||
<span class="agent-tag">shell</span>
|
<span class="agent-tag">shell</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -946,7 +947,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="solution-card">
|
<div class="solution-card">
|
||||||
<p class="card-label">Решение</p>
|
<p class="card-label">Решение</p>
|
||||||
<h3 class="card-title">spacesh разрывает эту связь.</h3>
|
<h3 class="card-title">spaceshell разрывает эту связь.</h3>
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
Сессиями владеет фоновый демон, а не окно. Интерфейс — всего лишь вид поверх него.
|
Сессиями владеет фоновый демон, а не окно. Интерфейс — всего лишь вид поверх него.
|
||||||
</p>
|
</p>
|
||||||
@@ -999,7 +1000,7 @@
|
|||||||
<h3 class="feature-title">CLI как первый класс</h3>
|
<h3 class="feature-title">CLI как первый класс</h3>
|
||||||
<p class="feature-text">
|
<p class="feature-text">
|
||||||
spacesh status --json, focus, new-surface, notify — те же команды, что и в интерфейсе, плюс shell-completions.
|
spacesh status --json, focus, new-surface, notify — те же команды, что и в интерфейсе, плюс shell-completions.
|
||||||
Встраивай spacesh в свои пайплайны.
|
Встраивай spaceshell в свои пайплайны.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-card reveal">
|
<div class="feature-card reveal">
|
||||||
@@ -1117,7 +1118,7 @@
|
|||||||
<div class="footer-inner">
|
<div class="footer-inner">
|
||||||
<div class="footer-left">
|
<div class="footer-left">
|
||||||
<span class="footer-logo">spaceshell.ru</span>
|
<span class="footer-logo">spaceshell.ru</span>
|
||||||
<span class="footer-copy">© 2026 spacesh</span>
|
<span class="footer-copy">© 2026 spaceshell</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="https://git.realmanual.ru/pub/spaceshell" target="_blank" rel="noopener">GitHub</a>
|
<a href="https://git.realmanual.ru/pub/spaceshell" target="_blank" rel="noopener">GitHub</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user