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]]
|
||||
name = "spacesh-cli"
|
||||
version = "0.1.7"
|
||||
version = "0.1.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -881,7 +881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spacesh-core"
|
||||
version = "0.1.7"
|
||||
version = "0.1.10"
|
||||
dependencies = [
|
||||
"alacritty_terminal",
|
||||
"serde",
|
||||
@@ -891,7 +891,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spacesh-proto"
|
||||
version = "0.1.7"
|
||||
version = "0.1.10"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"serde",
|
||||
@@ -903,7 +903,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spacesh-pty"
|
||||
version = "0.1.7"
|
||||
version = "0.1.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -913,7 +913,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spaceshd"
|
||||
version = "0.1.7"
|
||||
version = "0.1.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ members = [
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
version = "0.1.7"
|
||||
version = "0.1.10"
|
||||
|
||||
[workspace.dependencies]
|
||||
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])] = &[
|
||||
("claude", &["--continue"]),
|
||||
("codex", &["resume"]),
|
||||
("deepseek", &["resume"]),
|
||||
];
|
||||
|
||||
#[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}')
|
||||
SIDECAR_DIR := $(APP_DIR)/src-tauri/bin
|
||||
BUNDLE_CONFIG := src-tauri/tauri.bundle.conf.json
|
||||
APP_BUNDLE := $(APP_DIR)/src-tauri/target/$(TAURI_TARGET)/release/bundle/macos/spacesh.app
|
||||
NATIVE_APP_BUNDLE := $(APP_DIR)/src-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/spaceshell.app
|
||||
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
|
||||
@@ -19,7 +19,7 @@ REPO ?= spacesh
|
||||
|
||||
# ---- Gitea generic package registry (versioned .dmg downloads) ----
|
||||
GITEA_URL ?= https://git.realmanual.ru
|
||||
GITEA_OWNER ?= realmanual
|
||||
GITEA_OWNER ?= pub
|
||||
GITEA_PKG ?= spacesh
|
||||
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
|
||||
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/
|
||||
xattr -dr com.apple.quarantine /Applications/spacesh.app
|
||||
@echo "Installed (native). Quit & relaunch spacesh; the bundled daemon restarts."
|
||||
xattr -dr com.apple.quarantine /Applications/spaceshell.app
|
||||
@echo "Installed (native). Quit & relaunch spaceshell; the bundled daemon restarts."
|
||||
|
||||
.PHONY: install-universal
|
||||
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/
|
||||
xattr -dr com.apple.quarantine /Applications/spacesh.app
|
||||
xattr -dr com.apple.quarantine /Applications/spaceshell.app
|
||||
|
||||
.PHONY: reinstall
|
||||
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"); \
|
||||
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; \
|
||||
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"; \
|
||||
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
|
||||
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]]
|
||||
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 };
|
||||
}
|
||||
+9
-8
@@ -3,10 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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.">
|
||||
<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:url" content="https://spaceshell.ru">
|
||||
<meta property="og:image" content="https://spaceshell.ru/og.png">
|
||||
@@ -812,7 +812,7 @@
|
||||
<div class="header-inner">
|
||||
<a href="/" class="logo">
|
||||
<span class="logo-icon">>_</span>
|
||||
spacesh
|
||||
spaceshell
|
||||
</a>
|
||||
<nav class="nav">
|
||||
<a href="#features">Возможности</a>
|
||||
@@ -836,7 +836,7 @@
|
||||
Гоняй десяток AI-агентов параллельно. <span class="accent">Не теряй ни одного.</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
spacesh держит живые сессии Claude Code, Codex, Gemini и shell в фоновом демоне.
|
||||
spaceshell держит живые сессии Claude Code, Codex, Gemini и shell в фоновом демоне.
|
||||
Закрыл окно, обновил приложение, словил краш — агенты продолжают работать.
|
||||
</p>
|
||||
<div class="hero-buttons">
|
||||
@@ -860,7 +860,7 @@
|
||||
<span class="terminal-dot"></span>
|
||||
<span class="terminal-dot"></span>
|
||||
</div>
|
||||
<span class="terminal-title">spacesh — workspace</span>
|
||||
<span class="terminal-title">spaceshell — workspace</span>
|
||||
</div>
|
||||
<div class="terminal-grid">
|
||||
<div class="terminal-pane">
|
||||
@@ -928,6 +928,7 @@
|
||||
<span class="agent-tag">Codex</span>
|
||||
<span class="agent-tag">Gemini</span>
|
||||
<span class="agent-tag">opencode</span>
|
||||
<span class="agent-tag">deepseek</span>
|
||||
<span class="agent-tag">shell</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -946,7 +947,7 @@
|
||||
</div>
|
||||
<div class="solution-card">
|
||||
<p class="card-label">Решение</p>
|
||||
<h3 class="card-title">spacesh разрывает эту связь.</h3>
|
||||
<h3 class="card-title">spaceshell разрывает эту связь.</h3>
|
||||
<p class="card-text">
|
||||
Сессиями владеет фоновый демон, а не окно. Интерфейс — всего лишь вид поверх него.
|
||||
</p>
|
||||
@@ -999,7 +1000,7 @@
|
||||
<h3 class="feature-title">CLI как первый класс</h3>
|
||||
<p class="feature-text">
|
||||
spacesh status --json, focus, new-surface, notify — те же команды, что и в интерфейсе, плюс shell-completions.
|
||||
Встраивай spacesh в свои пайплайны.
|
||||
Встраивай spaceshell в свои пайплайны.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-card reveal">
|
||||
@@ -1117,7 +1118,7 @@
|
||||
<div class="footer-inner">
|
||||
<div class="footer-left">
|
||||
<span class="footer-logo">spaceshell.ru</span>
|
||||
<span class="footer-copy">© 2026 spacesh</span>
|
||||
<span class="footer-copy">© 2026 spaceshell</span>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="https://git.realmanual.ru/pub/spaceshell" target="_blank" rel="noopener">GitHub</a>
|
||||
|
||||
Reference in New Issue
Block a user