Commit graph

93 commits

Author SHA1 Message Date
Eliot M
2619484c8d feat(sidebar): old-Cial-style header with redeploy timestamp + collapse
Sidebar brand row now shows the running build's DD/MM HH:MM next to
the Cial logo (sourced from the system.version event already plumbed
through useDeployStatus) and a PanelLeftClose button to collapse the
panel. When collapsed, AppShell renders a floating hamburger button
at the top-left that re-opens it — mirrors the Old Cial pattern so
the workspace can claim the full width when desired.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-01 14:42:19 +00:00
Eliot MAURICE
96607f441b Add: all fixes 2026-05-01 16:33:18 +02:00
Eliot MAURICE
ef1b1ee31c Add: all fixes 2026-05-01 16:28:50 +02:00
Eliot M
f8c0b5fa69 fix(MessageList): guard against possibly-undefined message in lookup
`session.messages[i]` is typed as `Message | undefined` under
noUncheckedIndexedAccess. Optional-chain the role check so the
agent's lastUserIdx scan type-checks again — the runtime semantics
are identical (loop only runs while `i` is in range).
2026-05-01 14:06:14 +00:00
Eliot MAURICE
f380008659 Add: all fixes 2026-05-01 16:03:49 +02:00
Eliot M
36d2946a42 feat(sessions): live sidebar timer for any busy session
Adds a transient `turnStartedAt` to the Session schema (not persisted —
mixed in by the back from ProcessManager). chat.send now broadcasts
`session.updated` with `status='busy'` + `turnStartedAt` to every tab,
and process-bridge's finalize broadcasts the idle flip with
`turnStartedAt: null`. session.list and session.rename payloads are
decorated so the field is always coherent.

Front maps it onto `streamingStartedAt`, so the sidebar timer in
`SessionRow` now ticks for any in-flight session — across tabs, after
reload, and even when the user is viewing a different session.
2026-04-30 16:55:13 +00:00
Eliot M
bb3b4772a4 feat(sessions): rehydrate live in-flight buffer on reload
Persists a streaming-state snapshot to chat_session.streaming_state
every ~500 ms during a turn (text + thinking + collected tools), and
returns it from session.history (first page only) along with the
session status and turnStartedAt.

Front hydrates streamingText/Thinking/pendingTools/streamingStartedAt
from that payload when the session is busy, so a page reload mid-turn
no longer drops the active notch and pending tool calls — matches the
Old Cial reload behavior.
2026-04-30 15:54:00 +00:00
Eliot MAURICE
cbfd1d827b fix(session-view): wrap config picker in inner max-width container
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:38:12 +02:00
Eliot M
c0edc9c058 feat(self-edit): split deploy/restart + system.* lifecycle UI
Splits POST /self/deploy (build only, blocking) from POST /self/restart
(response-first when bouncing core-back) so the agent gets a clean
"build done" signal and can decide when to apply.

Adds system.deploying / system.deploy_done / system.restarting /
system.version WS events broadcast to all sockets, plus
GET /api/v1/system/version for post-restart confirmation.

UI: DeployBadge component renders building/restarting/reconnecting/
reloading/failed stages above the composer; Sidebar Cial logo gradient
mirrors the same lifecycle. AppShell shares one SessionsClient so the
deploy state machine and the session store consume the same socket.

BuildRunner snapshots .next/static/chunks to chunks-prev/ before next
build; edge falls back to chunks-prev (rewrite or direct disk read) so
already-open tabs survive the build window and hard-reload only after
a confirmed version mismatch on reconnect.

Skills (cial:build, cial:restart) updated for the new contract.
2026-04-30 14:24:31 +00:00
Eliot M
aad5dbf7ea fix(dev-tenant): NODE_ENV=production for built artefacts
`next build` always emits a production bundle, so serving it with
NODE_ENV=development at runtime mixes dev React with prod-built output
and surfaces as `useContext: null` during prerender (most visibly on
the synthetic `/global-error` route). Old-Cial parity is the dev tenant
running fully in production mode — `next dev`-style hot reload was
already gone after the watcher-removal commit, so the env-var was the
only remaining mismatch.

- supervisor.dev.ts: all four service children now spawn with
  NODE_ENV=production; rationale moved to the file's docblock so it
  isn't repeated four times.
- dev-entrypoint.sh: `pnpm turbo run build` runs with NODE_ENV=production
  too. The Dockerfile's image-level NODE_ENV=development is left intact
  so `pnpm install` keeps installing devDependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 13:53:20 +00:00
Eliot M
bf9df777fc fix(deploy): exclude edge from 'all' restart scope
After a normal `cial:build` redeploy, `restart('all')` was bouncing the
edge supervisor too — `process.exit(0)` on the supervisor causes Docker
to recreate the entire container, killing the agent's in-flight session
mid-deploy.

`expandRestartTargets('all')` now returns only the four service children
that load rebuilt application code (platform-front, platform-back,
core-front, core-back). Edge bounces only on an explicit `scope: 'edge'`
request. Updated `expectedRestartCount`, the broadcast targets list, and
the `edgeRestart` flag accordingly, plus all referencing docs and the
unrestricted restart skill.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 13:50:45 +00:00
Eliot M
37bfbce906 feat(dev-tenant): run from built artefacts, drop watchers
Old-Cial-style dev tenant. Source edits flow through cial:build →
BuildRunner → supervisor restart, the same path prod uses, instead of
relying on tsx watch / next dev --turbopack. Fixes the .next/ clobber
where a concurrent next build (scope=all) crashed the running next dev
core-front and tore the whole tenant down.

- dev-entrypoint.sh: pre-build the full workspace with `pnpm turbo run
  build` (was just protocol+sdk+edge); export CIAL_MONOREPO_ROOT for
  the next build --turbopack steps.
- supervisor.dev.ts: every child now spawns its built artefact
  (node dist/index.js, next start). Updated header doc accordingly.
- next.config.ts (both): refreshed comments — turbopack.root is only
  consulted at build time now, not by a running dev server.
- docs: updated dev-tenant, supervisor, and recipes to reflect the
  built-artefact model and remove HMR claims.
2026-04-30 13:15:56 +00:00
Eliot M
3a1df12cfa fix(next): use env-based turbopack.root, no runtime imports in config
Previous attempt added \`import path from 'node:path'\` + \`import.meta.url\`
to both next.config.ts files. Next 16's TS-config compiler emitted
CJS-style output (\`exports.default = ...\`), but the package.json
\`"type": "module"\` made Node load it as ESM, crashing on startup with:

  Failed to load next.config.ts
  ReferenceError: exports is not defined
      at <unknown> (next.config.compiled.js:2:23)

Switch to a single env var (\`CIAL_MONOREPO_ROOT\`) read at runtime —
no imports beyond the type-only NextConfig. Same Turbopack-root pin
as before, but the config file stays a literal object that Next's
TS compiler can emit as a no-op ESM module either way.

\`CIAL_MONOREPO_ROOT\` is now propagated to core-front and platform-front
in supervisor.dev.ts (core-back already had it). Outside the dev
container the var is undefined and Turbopack falls back to lockfile
auto-detection, which works fine without bind-mount interference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 10:43:44 +00:00
Eliot M
6b1254c817 fix(next): pin Turbopack workspace root for core-front + platform-front
Next 16's auto-detection of the workspace root (walking up looking for
pnpm-lock.yaml) misfires inside the dev container — when the anonymous
volume masking core/front/.next gets re-created on restart, Turbopack
mis-infers the project root as src/app/ and crashes with:

  Error: Next.js inferred your workspace root, but it may not be correct.
  We couldn't find the Next.js package (next/package.json) from the
  project directory: /cial/core/front/src/app

Set turbopack.root explicitly to the monorepo root (two levels up from
each next.config.ts) so detection can't go sideways. Recommended fix
per the error message itself.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 10:10:32 +00:00
Eliot M
e223ba45ec docs(self-edit): document synchronous /api/v1/self/deploy
Update API docs, recipes, design doc, deploy-pipeline architecture,
and deploy-logs ops doc to match the new synchronous behaviour
(commit 8505981). The endpoint now returns 200/500 with status,
durationMs, exitCode, errorSummary, and an inline logTail (last
~8KB) — no polling, no companion GET endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 14:56:48 +00:00
Eliot M
8505981889 feat(self-edit): make POST /api/v1/self/deploy synchronous
Old behaviour returned 202 with a deployId and expected the agent to
poll GET /deploy/:id — but that route lives on the Better-Auth-gated
deploy module, so localhost curl from inside the container got 401 on
every poll. The agent had no terminal signal and no log visibility,
matching the symptoms Eliot observed (stale-looking session, repeated
401s in core-back logs, no build output).

Mirror old-Cial's ergonomics: one synchronous request, blocks until
the build (and post-build restart) reach a terminal state, returns
{ ok, status, durationMs, exitCode, errorSummary, logTail } with HTTP
200 on success or 500 on failure. logTail is the last 8KB of the
deploy log file so the agent has the failure context inline without
needing a second round-trip.

- DeployService.waitForDone(deployId, timeoutMs): EventEmitter-based
  promise that resolves on the next 'done'/'cancelled' event for the
  given deployId, or immediately if the row is already terminal.
- DeployService.getLogPath(deployId) + DeployRepository.getLogPath:
  surface the persisted log_path for tail reading.
- self/router.ts: await waitForDone, read log tail, respond once.
- cial:build skills (restricted + unrestricted): drop the polling
  loop, document the synchronous response shape.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 14:44:47 +00:00
Eliot M
e61f1f1330 fix(tsconfig): route .tsbuildinfo into dist/ for protocol, sdk, back
By default tsc writes the incremental build info to
<package>/tsconfig.tsbuildinfo (next to the tsconfig). In restricted
dev mode /cial/core is RO, so this fails with EROFS — and only the
dist/ subdir is shadow-mounted as writable.

Setting tsBuildInfoFile to "dist/.tsbuildinfo" puts it inside the
writable shadow, matching the convention edge/tsconfig.json already
follows. No behavior change in unrestricted mode or prod (rw paths).
2026-04-29 13:57:11 +00:00
Eliot M
6f43c6a52d fix(dev-tenant): shadow core/* output paths in restricted mode
Restricted mode mounts /cial/core as RO, but core-front (Next.js
Turbopack) needs to write a lockfile to .next/, and the entrypoint's
pre-build writes tsc output to core/{protocol,sdk,edge}/dist. Without
shadows, the first restricted run after --reset crashes core-front
with EROFS on the lockfile.

Add anonymous volumes for the four writable paths under core/ when
--unrestricted is not set. Writes land in the ephemeral container
layer; the RO trust boundary on source files is preserved.

This was latent until the recent --reset flow exposed it (cached
.next/ + dist/ from prior unrestricted runs were masking the issue).
2026-04-29 13:53:13 +00:00
Eliot M
0a5ad20b8d feat(skills): mode-aware skill rendering driven by CIAL_UNRESTRICTED
Skills now templated under .claude/skills.src/<name>/{restricted,
unrestricted}.md and rendered to .claude/skills/<name>/SKILL.md by
core/scripts/render-skills.mjs based on the active mode. The agent
only sees the variant matching its actual permissions, so it no
longer has to branch on CIAL_UNRESTRICTED on every invocation.

- Dev: dev-entrypoint.sh re-renders on container start; dev-tenant.mjs
  masks /cial/.claude/skills with an anonymous volume so writes don't
  leak into the host repo bind mount.
- Prod: Dockerfile builder stage renders once (always restricted; prod
  doesn't expose --unrestricted).
- Static .claude/skills/ deleted from the repo and gitignored.
- docs/file-structure.md updated with the new layout.
2026-04-29 13:49:01 +00:00
Eliot M
a0c9973412 docs+fix: refresh /docs to match current container layout, fix BuildRunner filters
Audit pass over docs/ + adjacent code following the cial-* → core/platform/app
layout consolidation.

Bug fix:
- core/back BuildRunner ALL_FILTERS referenced @cial/core-back and
  @cial/core-front, which no longer exist (the packages are @cial/back +
  @cial/front). Self-edit deploys with scope=all would have silently
  skipped those packages. Filters corrected.

Docs aligned with reality:
- docs/README.md       — promotes file-structure.md to the start-here entry.
- architecture/dev-tenant.md  — full rewrite: paths now /cial/* throughout,
  documents the read-only :ro overlay of /cial/core, the new
  --config.confirm-modules-purge=false install flag, the symlink dance for
  project skills, and the agent's cwd=/cial + HOME=/cial/data/home setup.
- architecture/deploy-pipeline.md  — package-name fix for ALL_FILTERS.
- architecture/core-vs-platform.md — package-name fix for the build list.
- ops/supervisor.md     — drops stale "added in Phase 7" annotation.
- ops/deploy-logs.md    — example log line uses @cial/back.
- self-edit/recipes.md  — protocol path and dependency chain naming.
- design/self-edit-unrestricted.md — banner clarifying it's the original
  design record (pre-rename) so an agent doesn't follow stale paths from it.

Tiny code touch:
- core/edge/src/supervisor.dev.ts — comment on CIAL_MONOREPO_ROOT no longer
  contradicts itself ("not /cial" → "the bind-mounted repo at /cial").

Build verified: turbo run build for @cial/back still passes (cache miss
re-executed cleanly with the updated runner.ts).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 13:17:19 +00:00
Eliot M
341c6dd728 fix(dev-entrypoint): non-interactive pnpm install when modules volume is stale
After the cial-* → core/platform/app rename, the named modules volume
from any previous container run no longer matches the lockfile layout.
pnpm 9 detects this and prompts "remove and reinstall? (Y/n)" — which
stalls forever because the entrypoint runs without a TTY, leaving
node_modules empty and the next pre-build step crashing with
"Cannot find module 'zod'" (and friends).

Pass --config.confirm-modules-purge=false so pnpm just wipes and
reinstalls without asking. No behavior change on a fresh volume.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 13:09:14 +00:00
Eliot M
c50cc2b5fb refactor(layout): consolidate workspace under /cial — core/, platform/, app/
Reorganize the dev/prod tenant container so the agent runs in the monorepo
root with a clear, semantic directory tree:

  /cial/core/      — runtime (back, front, edge, ui, sdk, protocol, scripts,
                     docker). Locked down to the cial linux user (mode 0700
                     in prod; :ro bind mount in restricted dev).
  /cial/platform/  — agent-editable surface (back, front).
  /cial/app/       — App control plane sources, present in workspace but
                     never built or run inside the tenant container.
  /cial/docs/      — architecture + ops reference.
  /cial/.claude/   — project skills/agents/commands (symlinked into the
                     harness HOME by the dev entrypoint).
  /cial/data/      — persistent state (sqlite, deploy-logs, agent home).

Concrete changes:
- git mv cial-core → core, cial-platform → platform, cial-app → app,
  scripts → core/scripts.
- pnpm-workspace.yaml: packages now core/*, platform/*, app/*.
- Bulk path rewrites across 250+ source / docker / docs files.
- core/scripts/dev-tenant.mjs: ROOT path fix, rw mount of repo + ro
  overlay of /cial/core when --unrestricted is not set (FS-level
  trust boundary, defense in depth).
- core/edge/src/supervisor.{ts,dev.ts}: cwd + CLAUDE_HOME relocated to
  /cial/data/home; agent runs from /cial root so skill discovery picks
  up /cial/.claude/skills automatically.
- core/back providers/claude.ts: HOME defaults to /cial/data/home, cwd
  defaults to /cial.
- core/docker/{Dockerfile,Dockerfile.dev,dev-entrypoint.sh}: COPY +
  WORKDIR + ENTRYPOINT updated; .claude → harness symlink.
- app/docker/{Dockerfile,Dockerfile.router}: COPY core, COPY app
  (instead of cial-core / cial-app).
- New docs/file-structure.md — single canonical map of the runtime
  layout. cial:self-edit SKILL.md mandates reading it first.
- cial:build SKILL.md: scope notes updated to platform/* and core/*.
- root package.json: smoke / dev:tenant scripts now under core/scripts/.
- core/scripts/smoke.mjs: cial-core.db → cial.db.

Externals preserved as-is by intent:
- JWT issuer string 'cial-app' in core/back/src/modules/sso/index.ts +
  app/api/src/lib/sso.ts is an external contract — NOT renamed.
- @cial/back / @cial/edge / @cial/protocol / @cial/sdk / @cial/front
  package names kept stable to minimize blast radius.

Verified:
- pnpm install --prod=false → ok
- turbo run build for protocol, sdk, back, edge, front, platform-back,
  platform-front → all 7 successful (Next builds + tsc clean).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 13:04:45 +00:00
Eliot M
fd165fdea9 fix(dev-tenant): symlink project .claude/{skills,agents,commands} into harness HOME
The Claude harness spawned by sessions/process/providers/claude.ts runs with
HOME=$CLAUDE_HOME (= /var/lib/cial/claude-home) and cwd=$HOME by default.
Slash-command discovery walks up from cwd looking for .claude/skills/, but
nothing under /var/lib/cial/... sees /workspace/.claude/. Result: skills
shipped in the repo (cial:self-edit, cial:build, cial:restart) appeared as
"Unknown command".

Entrypoint now symlinks the bind-mounted project .claude/* into the harness
HOME so they're discovered at the user level. Symlinks (not copies) so live
edits via the bind mount take effect on next session spawn without rebuild.
2026-04-29 11:08:22 +00:00
Eliot M
6549187afb phase(6c): add cial:build + cial:restart skills, widen protocol restart targets
Adds the two utility skills the agent invokes from the master cial:self-edit
flow: cial:build POSTs /api/v1/self/deploy and polls until terminal,
cial:restart POSTs /api/v1/self/restart and handles the edge-bounce case.
Both target core-back directly on :4000 because edge only proxies /.cial/api/*,
not /api/v1/*.

Widens DeployStartEvent.targets, DeployRestartStartEvent.service, and
DeployRestartDoneEvent.service in @cial/protocol to include core-front,
core-back, and edge so the broadcast events emitted during scope=all
deploys typecheck. Trust enforcement still lives in the supervisor, not
the schema.
2026-04-29 10:49:42 +00:00
Eliot M
9c1f551ed6 phase(6c): self-edit/build/restart endpoints + --unrestricted dev mode
Adds POST /api/v1/self/deploy and /api/v1/self/restart on cial-core for
agent-initiated builds and restarts. Introduces CIAL_UNRESTRICTED=1
(opt-in via `pnpm dev:tenant --unrestricted`) which widens the trust
boundary so the agent can rebuild and restart core+sdk+protocol+edge
in addition to platform.

Trust boundary enforced at three layers:
- BuildRunner pnpm filter (platform vs all)
- Supervisor IPC RESTARTABLE_SERVICES set
- localhost-only middleware on /api/v1/self/*

Edge restart uses 50ms-deferred process.exit so Docker restart-policy
bounces the container. Dev supervisor gained the IPC server it was
silently missing.

Ships docs/ tree (architecture, self-edit, ui, ops) and the
cial:self-edit Claude skill, both copied into the dev+prod images so
the in-container agent can read them before editing.
2026-04-29 10:46:13 +00:00
Eliot M
403e179b04 ui(parity): old-Cial visual parity pass — sidebar timer, geometry, copy + cost
Sidebar:
- SessionRow → rounded-xl + py-1.5 + mb-0.5; 3px indigo active bar with glow
- Port SidebarTimer (live MM:SS in indigo busy pill) reading streamingStartedAt
- Replace 7×7 + icon with full-width "New session" CTA; drop Sessions caption

Chat:
- MessageList: drop hardcoded max-w-5xl so messages span full pane
- StatsFooter: surface costUsd via new fmtCost (Coins icon)
- MessageRow: hover-revealed Copy button next to AssistantHeader (Copy → Check)
- InputBar: drop "Cial can make mistakes…" disclaimer (not in old)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 09:41:08 +00:00
Eliot M
b0490e713b phase(6b): deploy controller — build, restart, log streaming, mode toggle
- protocol: Zod schemas for deploy REST DTOs and WS events
- back/deploy module: migrate (deploy + tenant_settings), repository,
  supervisor-client (UDS JSONL), single-flight BuildRunner with cancel,
  DeployService bridging runner events to WS broadcasts
- REST endpoints under /deploy (instance_admin gated): start, restart-only,
  cancel, mode get/patch, get/list
- WS handshake handlers for DeployWatch/DeployUnwatch
- bootstrap: build deploy module once with ws.pool, inject router into
  createApp via AppRouters; attach request listener after wiring
- config: monorepoRoot, supervisorSocket, deployLogDir, platformRoot
2026-04-27 00:25:18 +00:00
Eliot M
ed0622cd38 phase(6a): supervisor selective-restart IPC + Phase 6 design spec
Adds a Unix-socket protocol (default /run/cial-supervisor.sock, mode 0660
cial:cial) so Core Back can ask the supervisor to bounce platform-front /
platform-back without taking the container down. Only platform-* children
are restartable; core-back / core-front / edge are rejected. Exit handler
distinguishes requested restarts (respawn) from crashes (existing
crash-the-container behaviour preserved).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 00:12:50 +00:00
Eliot M
b3be40e8f8 ui(stream): phase-themed dot left of Cial + drop blinking caret + align body
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:59:21 +00:00
Eliot M
a41a7a4b26 feat(messages): collapsed Process dropdown for completed answers
Replaces the always-visible ToolList stack with a single
"Process (N steps)" pill below each finished assistant message.
Click to expand a panel listing reasoning + every tool call (with
input/output) + response, mirroring the legacy Cial UX.
Multi-turn answers show "(X turns, X steps)" and group by turn.
SDK re-exports SubTurn so the UI can type the metadata.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:51:37 +00:00
Eliot M
7a122f4776 fix(stream): notch ignores done tools + timeline lives under Cial header
- getStatusLabel + ActiveNotch now filter pendingTools by output==null
  before picking the "active" call, so the notch correctly flips to
  Writing once tools settle (was sticking on the last tool's name).
- Streaming timeline moved inside StreamingBubble / ThinkingBubble,
  rendered between AssistantHeader and the body — matches legacy Cial
  layout where tools sit under "Cial" and above the answer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:45:12 +00:00
Eliot M
d7e74a6986 feat(stream): jittered char reveal + subtle tool timeline
- StreamingBubble: paced reveal now varies 0.6×–1.4× per frame so the
  typewriter feels organic instead of metronomic.
- StreamingTimeline: subtle vertical dots+line of pending/done tool
  calls (label · check · duration), shown above the streaming
  response. Adds a "Writing response" step once tools settle. Last 3
  visible by default; older steps collapse behind "+ N earlier
  steps". Replaces the heavy ToolList during streaming — full
  ToolPills stay for completed messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:39:32 +00:00
Eliot M
022b46f022 feat(stream): paced character reveal for assistant text
Decouples visible text from upstream chunk size. A rAF loop reveals
characters at ~220 chars/sec regardless of how big each backend
delivery is, with a burst-catch-up when the backlog grows past 600
chars so long answers don't fall behind. Resets on new turn.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:33:07 +00:00
Eliot M
a8574c311c perf(stream): tail harness log at 50ms + memo streaming markdown
Drops TAIL_POLL_MS from 500ms to 50ms (env-overridable) so text
flushes ~20 Hz instead of 2 Hz. fs.statSync + readSync on the
per-turn log is negligible. StreamingBubble now memoises
renderMarkdown(text) so the tighter tick rate doesn't reparse the
buffer on every parent re-render.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:26:33 +00:00
Eliot M
25f519f8d1 ui(composer): float model/effort picker above input + widen chat
Picker now lives outside the composer shell as its own subtle pill row,
solid background (not transparent) so it reads as a real control.
Chat content widens to max-w-5xl while the input stays max-w-3xl
centered, matching the legacy Cial layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:18:36 +00:00
Eliot M
6a5a283bb7 fix(picker): align effort levels + model list with Anthropic docs
Per platform.claude.com (models overview) and code.claude.com
(--effort flag in CLI reference):

- effort enum: drop 'normal' (not a CLI value); add 'low', 'medium',
  'max' so the wire matches the official 5-level set
  ('low' | 'medium' | 'high' | 'xhigh' | 'max')
- models.ts: surface all 5 effort options in the picker, strongest
  first; default stays 'xhigh'
- models.ts: add legacy models (Opus 4.6, Sonnet 4.5, Opus 4.5,
  Opus 4.1) as picker options tagged legacy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:10:41 +00:00
Eliot M
669b594094 feat(ui): model + effort picker in composer (defaults Opus 4.7 / xhigh)
- protocol: extend SessionEffort enum with 'xhigh' (matches legacy max)
- back/service: new sessions default effort to 'xhigh' so the harness
  always boots at max reasoning unless the user picks otherwise
- sdk: chatSend + create accept 'xhigh' on the effort union, plus
  effort field on create() so per-session config can be set up front
- ui/store: thread per-turn model + effort overrides through
  sendMessage/newSession; expose ChatSession.effort to consumers
- ui: new ConfigPicker (model + effort dropdowns) embedded in InputBar,
  with state owned by SessionView and synced from active session

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:05:21 +00:00
Eliot M
565439aebd fix(ui): persist tool calls + show live model/usage + reasoning panel
- store: stream.tool_result merges output+durationMs onto the existing
  tool entry (was filter, causing tools to flash and disappear mid-stream)
- store: subscribe stream.start to patch session.model from the wire
- store: subscribe stream.usage and surface live token counts via new
  ChatSession.streamingUsage; cleared on stream end
- ActiveNotch: render live "{X} in · {Y} out" tokens alongside model
- MessageRow: collapsible ReasoningBubble for completed metadata.thinking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 22:52:25 +00:00
Eliot M
4b79bdadc1 feat(ui): timestamps, stats footer, collapsible tool stack
- Cap `ToolList` at 4 visible pills; older calls collapse behind a
  paper-stack "+ N more tool calls" button (mirrors legacy UX). Click to
  expand inline. Stack styles in the new `Tools.scss`.
- Add timestamp on every message row — right-aligned "You · 14:32" above
  the user bubble; "Cial · 14:32" inline in the assistant header.
  Timestamps use `Date.now()` ms (no `* 1000` like legacy).
- New `StatsFooter` rendered under each completed assistant message:
  short model · turn count · duration · tokens (in/out). Driven by
  `message.metadata.stats`.
- New `lib/format.ts` helpers: `formatTime`, `fmtTokens`. SDK now also
  re-exports `TurnStats` / `TurnUsage` for downstream consumers.
2026-04-26 22:38:29 +00:00
Eliot M
4899c2d120 feat(ui): render tool calls in chat (legacy parity)
- `lib/tools.ts` — `toolLabel(tool)` produces a human label across the
  Claude/Kimi/Gemini name variants ("Reading file.ts", "$ ls", "Grep …",
  "Skill: /foo", "mcp__server: action"). `toolIcon(name)` returns the
  matching lucide icon with a per-category color. Plus `fmtDuration` and
  `isPendingTool` helpers.
- `components/Tools/ToolPill.tsx` — single expandable tool row built on
  `<details>` (kbd + a11y free). Header shows icon + label + spinner/check
  + duration; body shows pretty-printed input then output (max 4kB
  truncated, scrollable).
- `components/Tools/ToolList.tsx` — animated vertical stack of pills, used
  both for completed messages (`message.metadata.tools`) and live
  streaming (`session.pendingTools` with `animated`).
- Wired into `MessageRow` (assistant tools) and `MessageList` (pending
  tools below the streaming bubble).
2026-04-26 22:31:03 +00:00
Eliot M
c43b8950c0 fix(ui): chat parity with legacy — optimistic streaming, model + timer, floating input, fixed scroll
- Add `streamingStartedAt` + `model` to `ChatSession`. The store sets
  `streamingStartedAt` synchronously on `sendMessage` so the ActiveNotch
  appears the instant the user submits and stays visible across the gaps
  between tool calls (it used to flicker because the previous check ANDed
  `streamingText`, `pendingTools`, and `status === 'busy'` — all three lag
  behind submit). Cleared on `stream.message` / `stream.cancelled` /
  `stream.error`.
- ActiveNotch now uses `streamingStartedAt` for the live timer (was using
  `updatedAt` which ticks on every server event) and renders the short
  model id next to it via a new `lib/format.ts#shortModel`.
- MessageList scroll: switch to `requestAnimationFrame` + `instant`
  during streaming so successive token chunks don't fight an in-flight
  smooth scroll; smooth otherwise. Also include `pendingTools.length` in
  the change-detection so the notch movement triggers a scroll.
- Float the InputBar absolutely over the bottom of SessionView so the
  chat takes the full height of the pane while the composer stays
  centered. Add `pb-44` to MessageList content for the overlap.
2026-04-26 22:25:15 +00:00
Eliot M
d7c83173cf refactor(core-ui): structured package — components/lib/styles + SCSS bundle
Reorganize @cial/core-ui from a flat src/ into three folders:
  - components/<Name>/  — tsx + colocated .scss per component, splitting
                         MessageList (240→60 lines + 6 leaf files) and
                         Sidebar / SessionView into their natural pieces
  - lib/                — store, theme, markdown, types
  - styles/             — _tokens, _base, _animations, _components,
                         index.scss (the SCSS bundle entry)

Move all custom CSS (theme tokens, glow border, hljs theme, prose-chat,
keyframes, .cial-logo, .bubble-*, .composer-shell, .send-button,
.cial-avatar, .empty-state-icon …) into the package's SCSS partials so
the package travels with its own styles. Replace inline gradient
style={{ background: 'linear-gradient(...)' }} usages in AppShell,
Sidebar avatar, SessionView empty-state and InputBar send button with
proper SCSS classes.

Expose `@cial/core-ui/styles` via the package.json exports map and
import it from cial-platform/front via a tiny cial-styles.scss alongside
globals.css. globals.css now owns ONLY Tailwind setup (the framework
import, @source for cross-package class scanning, @theme inline tokens
and the two @utility scrollbar rules — none of which Sass can parse).

Add `sass ^1.83.4` as a dev dep on cial-platform/front; Next.js 16
picks it up automatically. No public API change — `AppShell` and
`SessionUser` are still the only exports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 22:12:24 +00:00
Eliot M
0ae4b358dc feat(ui): markdown rendering for assistant messages (parity with legacy Cial)
Slim port of /app/client markdown pipeline: marked + highlight.js core
with 10 curated languages, custom renderers for code blocks (with copy
button), tables, lists, headings, blockquotes, links, codespan, hr,
image. Tiny LRU render cache so streaming re-renders stay cheap.

MessageList now wraps assistant + streaming bubbles in RenderedContent
(prose-chat) instead of whitespace-pre-wrap. Adds --t-code-bg /
--t-code-text vars + dark/light hljs theme rules in globals.css.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 21:59:38 +00:00
Eliot M
3caaaf9a83 fix(ui): InputBar overflow + ActiveNotch phase parity with legacy Cial
InputBar overflow:
- SessionView's active branch used h-full inside a flex container that
  also held InputBar as a sibling, pushing the composer below the
  visible viewport. Replaced with `flex-1 min-h-0` so the message area
  shares space with the composer.

ActiveNotch phase parity:
- Re-export Tool from @cial/sdk.
- ChatSession now tracks `pendingTools: ReadonlyArray<Tool>`, populated
  from `stream.tool_use` and drained on `stream.tool_result`. Cleared
  on `stream.message`/cancelled/error.
- theme.ts now mirrors legacy /app/client activityTheme.ts: 9-phase
  palette (Thinking/Writing/Researching/Editing/Executing/Browsing/
  Delegating/Running skill/Working) with phase-specific colours +
  glow + dot, plus `getStatusLabel(streaming)` derivation rule
  (Read/Glob/Grep → Researching, Edit/Write → Editing, Bash →
  Executing, WebSearch/WebFetch → Browsing, Agent/Task → Delegating,
  Skill → Running skill).
- ActiveNotch reads phase via getStatusLabel + getActivityTheme and
  shows the active tool name beside the phase label on md+ screens.
2026-04-26 21:52:00 +00:00
Eliot M
41f4566c7c feat(ui): polished chat shell with Tailwind 4 + motion + floating ActiveNotch
Replaces the inline-CSS placeholder UI with a full Tailwind 4 + motion/react
chat surface:

- AppShell uses theme tokens from globals.css (no more dangerouslySetInnerHTML)
- Sidebar: animated brand, motion-driven session row hover/delete reveal,
  layoutId active bar, lucide icons, connection dot
- MessageList: motion entry on bubbles, streaming bubble with blinking
  caret, sticky floating ActiveNotch overlay (animated glow border + live
  timer) shown during streaming
- InputBar: composer-shell with focus ring, motion send/stop button morph
  with spring, auto-resize textarea
- Tailwind 4 wired into platform-front via @tailwindcss/postcss + @source
  directive pointing at the cial-core/ui workspace package
2026-04-26 18:49:31 +00:00
Eliot M
452ed425d3 fix(claude): set IS_SANDBOX=1 so --dangerously-skip-permissions works as root
Inside the tenant container claude runs as root, and refuses
`--dangerously-skip-permissions` with "cannot be used with root/sudo
privileges for security reasons". IS_SANDBOX=1 is its documented bypass
— same flag the legacy server used.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 18:33:26 +00:00
Eliot M
30033c31f6 fix(dev:tenant): exec compiled supervisor.dev.js (tsx isn't a root dep)
The entrypoint tried `pnpm exec tsx supervisor.dev.ts` but tsx is only
in cial-core/back + cial-platform/back devDeps, not at the workspace
root — `tsx not found`. The edge prebuild (`tsc -b`) already compiles
supervisor.dev.ts → dist/supervisor.dev.js, so just node-exec the JS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 18:29:55 +00:00
Eliot M
bcf2d2b9c3 feat(dev:tenant): replace native dev mode with Docker single-tenant container
dev:tenant now runs a single Linux container that mirrors the prod
tenant container shape (5 processes, port 8080) but with hot reload,
single root user, and credentials injected from the host's macOS
Keychain on startup. Source is bind-mounted; Linux-built node_modules
live in named volumes so they don't collide with macOS-built ones.

New files:
  cial-core/docker/Dockerfile.dev      single-user dev image (claude binary baked in)
  cial-core/docker/dev-entrypoint.sh   creds → ~/.claude → pnpm install → pre-build → exec supervisor
  cial-core/edge/src/supervisor.dev.ts container-side supervisor with watchers

Rewritten:
  scripts/dev-tenant.mjs               extracts host keychain + drives docker build/run

Volumes (survive Ctrl-C, wiped by `node scripts/dev-tenant.mjs --reset`):
  cial-dev-tenant-state       /var/lib/cial (sqlite db, claude home)
  cial-dev-tenant-modules     /workspace/node_modules + per-package shadows
  cial-dev-tenant-pnpm-store  /pnpm-store (install cache)

Trade-offs:
  - First boot is slow (pnpm install in-container). Subsequent boots fast.
  - protocol/sdk/edge dist files are written through the bind mount to the host.
  - macOS Keychain stays the source of truth for credentials; the container's
    OAuth refreshes don't propagate back.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 18:27:08 +00:00
Eliot M
997d0d8618 fix(claude): scan projects/ for any cwd-encoded subdir on resume
hasLocalState was hardcoded to look at `~/.claude/projects/-root/<id>.jsonl`
— claude's path-encoding of /root, which only matches the legacy server's
container cwd. On macOS the encoded subdir is `-Users-<user>`, so the
check always returned false → engine fell back to `--session-id` for an
id that already existed → claude silently collided and the second turn
hung.

Scan every subdir of projects/ instead. Also bump the spawn log to
info-level and warn on non-zero exit so harness errors are visible
in dev:tenant output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 18:02:58 +00:00
Eliot M
57e6e119da fix(sessions): fall back to parent HOME when CLAUDE_HOME unset
The previous fallback (`<coreDataDir>/claude-home`) forced ClaudeProvider
to override HOME even in dev:tenant — re-introducing the macOS Keychain
breakage that the dev-tenant fix was supposed to resolve. Production
containers always set CLAUDE_HOME explicitly per tenant, so the fallback
only applies to developer boxes, where pass-through HOME is what we want.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 17:59:08 +00:00