Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/workspaces/templates/auto-quant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Each workspace gets its own `user_data/data/`. First run, the agent runs `uv run
## Parameters

- **Tag** — becomes the branch name (`autoresearch/<tag>`).
- **Agents** — default Claude; Codex works too if you prefer.

All available CLI runtimes are enabled (Claude, Codex, shell). The `+` new-session button defaults to Claude.

Power-user override: set `AQ_TEMPLATE_DIR` in the launcher env to point at a pre-existing Auto-Quant clone (e.g. one you've already populated with data).
2 changes: 1 addition & 1 deletion src/workspaces/templates/auto-quant/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"displayName": "Auto-Quant",
"groupOrder": 20,
"description": "Auto-Quant autoresearch workspace — clones the public Auto-Quant repo on first use (~/.openalice/workspaces/auto-quant-mirror), then makes per-workspace local clones with an autoresearch/<tag> branch and symlinked .feather data. Set AQ_TEMPLATE_DIR to override the source location.",
"defaultAgents": ["claude"]
"defaultAgents": ["claude", "codex"]
}
3 changes: 1 addition & 2 deletions src/workspaces/templates/chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,5 @@ Things Alice will route here:

When spawning, you'll configure:
- **Tag** — short identifier for this workspace (lowercase, dashes ok).
- **Agents** — which CLI runtimes to enable (default: Claude + Codex).

That's it. No template-specific parameters — everything else is shaped by what you say to the agent.
That's it. All available CLI runtimes (Claude, Codex, shell) are enabled by default; the template's first listed adapter is what the `+` "new session" button defaults to.
3 changes: 2 additions & 1 deletion src/workspaces/templates/finance-research/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Both layers load the same SKILL.md trees (market-analysis, social-readers, data-
## Parameters

- **Tag** — short identifier for this workspace.
- **Agents** — default Claude + Codex (both discover the same skill trees).

All available CLI runtimes are enabled (Claude, Codex, shell — both Claude and Codex discover the same skill trees). The `+` new-session button defaults to Claude.

Finance-skills is cloned fresh on every spawn — no shared cache. Keeps upstream traffic visible to its maintainer, who's part of the ecosystem we want to grow.
102 changes: 25 additions & 77 deletions ui/src/components/workspace/ChatWorkspaceSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@ import { useWorkspaces } from '../../contexts/WorkspacesContext'
import { useWorkspace } from '../../tabs/store'
import { getFocusedTab } from '../../tabs/types'
import { ConfirmDialog } from '../ConfirmDialog'
import { createWorkspace, deleteWorkspace, type SessionRecord, type Workspace } from './api'
import { deleteWorkspace, type SessionRecord, type Workspace } from './api'
import { SessionRow } from './Sidebar'
import { useCreateWorkspace } from '../../hooks/useCreateWorkspace'

const CHAT_TEMPLATE = 'chat'
const TAG_HINT = 'a-z, 0-9, "-", "_", up to 33 chars'
const TAG_RE = /^[a-z0-9][a-z0-9_-]{0,32}$/

function defaultTagFor(workspaces: readonly Workspace[]): string {
const now = new Date()
Expand Down Expand Up @@ -63,32 +62,23 @@ export function ChatWorkspaceSection(): ReactElement | null {
const chatTemplate = ctx.templates.find((t) => t.name === CHAT_TEMPLATE)

const [showCreate, setShowCreate] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [tag, setTag] = useState('')
const [pickedAgents, setPickedAgents] = useState<Set<string> | null>(null)
const [createError, setCreateError] = useState<string | null>(null)
const inputRef = useRef<HTMLInputElement | null>(null)
const [pendingDelete, setPendingDelete] = useState<Workspace | null>(null)

const checkedAgents: ReadonlySet<string> = useMemo(() => {
if (pickedAgents) return pickedAgents
return new Set(chatTemplate?.defaultAgents ?? ['claude'])
}, [pickedAgents, chatTemplate])

const toggleAgent = (id: string): void => {
setPickedAgents((prev) => {
const base = prev ?? new Set(chatTemplate?.defaultAgents ?? ['claude'])
const next = new Set(base)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const create = useCreateWorkspace({
template: CHAT_TEMPLATE,
templateDefaultAgents: chatTemplate?.defaultAgents,
availableAgents: ctx.agents,
onCreated: (workspace) => {
closeCreate()
ctx.refresh()
openOrFocus({ kind: 'workspace', params: { wsId: workspace.id } })
},
})

const openCreate = (): void => {
setShowCreate(true)
setTag(defaultTagFor(ctx.workspaces))
setCreateError(null)
create.setTag(defaultTagFor(ctx.workspaces))
// Focus + select on next paint so users can type to replace the
// default in one keystroke.
setTimeout(() => {
Expand All @@ -99,34 +89,12 @@ export function ChatWorkspaceSection(): ReactElement | null {

const closeCreate = (): void => {
setShowCreate(false)
setTag('')
setPickedAgents(null)
setCreateError(null)
create.reset()
}

const submit = async (e: FormEvent<HTMLFormElement>): Promise<void> => {
e.preventDefault()
const t = tag.trim()
if (!TAG_RE.test(t)) {
setCreateError(`invalid tag (${TAG_HINT})`)
return
}
if (checkedAgents.size === 0) {
setCreateError('pick at least one agent')
return
}
setSubmitting(true)
setCreateError(null)
const result = await createWorkspace(t, CHAT_TEMPLATE, Array.from(checkedAgents))
setSubmitting(false)
if (result.ok) {
closeCreate()
ctx.refresh()
openOrFocus({ kind: 'workspace', params: { wsId: result.workspace.id } })
} else {
const msg = result.error.message ?? result.error.error ?? `HTTP ${result.status}`
setCreateError(msg)
}
await create.submit()
}

const handleConfirmDelete = async (): Promise<void> => {
Expand All @@ -140,10 +108,10 @@ export function ChatWorkspaceSection(): ReactElement | null {
}

useEffect(() => {
if (showCreate && tag === '' && chatTemplate) {
setTag(defaultTagFor(ctx.workspaces))
if (showCreate && create.tag === '' && chatTemplate) {
create.setTag(defaultTagFor(ctx.workspaces))
}
}, [showCreate, tag, chatTemplate, ctx.workspaces])
}, [showCreate, create, chatTemplate, ctx.workspaces])

if (!chatTemplate) return null

Expand Down Expand Up @@ -175,44 +143,24 @@ export function ChatWorkspaceSection(): ReactElement | null {
ref={inputRef}
type="text"
placeholder="tag (e.g. may1)"
value={tag}
onChange={(e) => setTag(e.target.value)}
disabled={submitting}
value={create.tag}
onChange={(e) => create.setTag(e.target.value)}
disabled={create.creating}
spellCheck={false}
autoCorrect="off"
autoCapitalize="off"
className="flex-1 min-w-0 px-2 py-1 text-[12px] rounded border border-border bg-bg text-text placeholder:text-text-muted/60 focus:outline-none focus:border-accent"
/>
<button
type="submit"
disabled={submitting || tag.length === 0}
disabled={create.creating || create.tag.length === 0}
className="px-2.5 py-1 text-[12px] rounded bg-accent text-white disabled:opacity-40 hover:bg-accent/90"
>
{submitting ? '…' : 'create'}
{create.creating ? '…' : 'create'}
</button>
</div>
{ctx.agents.length > 0 && (
<div className="flex flex-wrap gap-2 text-[11px] text-text-muted">
{ctx.agents.map((a) => (
<label
key={a.id}
className="flex items-center gap-1 cursor-pointer"
title={a.displayName}
>
<input
type="checkbox"
checked={checkedAgents.has(a.id)}
onChange={() => toggleAgent(a.id)}
disabled={submitting}
className="w-3 h-3"
/>
<span>{a.id}</span>
</label>
))}
</div>
)}
{createError && (
<div className="text-[11px] text-red">{createError}</div>
{create.error && (
<div className="text-[11px] text-red">{create.error}</div>
)}
</form>
)}
Expand Down
95 changes: 20 additions & 75 deletions ui/src/components/workspace/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { FormEvent, ReactElement } from 'react';
import { Cpu, LayoutGrid, Library, Sparkles, Terminal, type LucideIcon } from 'lucide-react';

import {
createWorkspace,
deleteWorkspace,
type AgentInfo,
type SessionRecord,
type TemplateInfo,
type Workspace,
} from './api';

const TAG_HINT = 'a-z, 0-9, "-", "_", up to 33 chars';
const TAG_RE = /^[a-z0-9][a-z0-9_-]{0,32}$/;
import { useCreateWorkspace } from '../../hooks/useCreateWorkspace';

export interface Selection {
readonly wsId: string;
Expand Down Expand Up @@ -50,11 +47,7 @@ export interface SidebarProps {
}

export function Sidebar(props: SidebarProps): ReactElement {
const [creating, setCreating] = useState(false);
const [tag, setTag] = useState('');
const [template, setTemplate] = useState<string>('');
const [pickedAgents, setPickedAgents] = useState<Set<string> | null>(null);
const [createError, setCreateError] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);

useEffect(() => {
Expand All @@ -65,49 +58,19 @@ export function Sidebar(props: SidebarProps): ReactElement {
}, [props.templates, template]);

const selectedTemplate = props.templates.find((t) => t.name === template);
const checkedAgents: ReadonlySet<string> = useMemo(() => {
if (pickedAgents) return pickedAgents;
return new Set(selectedTemplate?.defaultAgents ?? ['claude']);
}, [pickedAgents, selectedTemplate]);

const toggleAgent = (id: string): void => {
setPickedAgents((prev) => {
const base = prev ?? new Set(selectedTemplate?.defaultAgents ?? ['claude']);
const next = new Set(base);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
const create = useCreateWorkspace({
template,
templateDefaultAgents: selectedTemplate?.defaultAgents,
availableAgents: props.agents,
onCreated: (workspace) => {
props.onChanged();
props.onSelectWorkspace(workspace.id);
},
});

const submit = async (e: FormEvent<HTMLFormElement>): Promise<void> => {
e.preventDefault();
const t = tag.trim();
if (!TAG_RE.test(t)) {
setCreateError(`invalid tag (${TAG_HINT})`);
return;
}
if (template === '') {
setCreateError('no template selected');
return;
}
if (checkedAgents.size === 0) {
setCreateError('pick at least one agent');
return;
}
setCreating(true);
setCreateError(null);
const result = await createWorkspace(t, template, Array.from(checkedAgents));
setCreating(false);
if (result.ok) {
setTag('');
setPickedAgents(null);
props.onChanged();
props.onSelectWorkspace(result.workspace.id);
} else {
const msg = result.error.message ?? result.error.error ?? `HTTP ${result.status}`;
setCreateError(msg);
}
await create.submit();
};

const onDelete = async (id: string): Promise<void> => {
Expand Down Expand Up @@ -138,11 +101,8 @@ export function Sidebar(props: SidebarProps): ReactElement {
<select
className="sidebar-template-select"
value={template}
onChange={(e) => {
setTemplate(e.target.value);
setPickedAgents(null);
}}
disabled={creating}
onChange={(e) => setTemplate(e.target.value)}
disabled={create.creating}
title={selectedTemplate?.description ?? ''}
>
{props.templates.map((t) => (
Expand All @@ -156,33 +116,18 @@ export function Sidebar(props: SidebarProps): ReactElement {
ref={inputRef}
type="text"
placeholder="tag (e.g. may1)"
value={tag}
onChange={(e) => setTag(e.target.value)}
disabled={creating}
value={create.tag}
onChange={(e) => create.setTag(e.target.value)}
disabled={create.creating}
spellCheck={false}
autoCorrect="off"
autoCapitalize="off"
/>
<button type="submit" disabled={creating || tag.length === 0}>
{creating ? '…' : 'create'}
<button type="submit" disabled={create.creating || create.tag.length === 0}>
{create.creating ? '…' : 'create'}
</button>
{props.agents.length > 0 && (
<div className="sidebar-create-agents">
{props.agents.map((a) => (
<label key={a.id} className="sidebar-agent-toggle" title={a.displayName}>
<input
type="checkbox"
checked={checkedAgents.has(a.id)}
onChange={() => toggleAgent(a.id)}
disabled={creating}
/>
<span>{a.id}</span>
</label>
))}
</div>
)}
</form>
{createError && <div className="sidebar-error">{createError}</div>}
{create.error && <div className="sidebar-error">{create.error}</div>}

<ul className="sidebar-list">
{props.onOpenOverview && (
Expand Down
Loading
Loading