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
97 changes: 97 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,98 @@
# Changelog

## 7.7.0

Three things that were true only in one place become true everywhere: the file
browser now works on remote projects, a remote run finds the same `node` an
interactive shell would, and the dials a new chat opens on are yours to set.

### New-chat defaults

- **Five dials, set once.** Mode, agent mode, model, thinking effort and turn
budget now have a configured default instead of a literal buried in the markup.
Settings owns the global half; a project can override any subset of the five.
The chain is **project override → global default → built-in**, every row shows
which of the three answered, and each has a one-click Reset.

- **A project stores only the dials it pinned.** Storing all five instead would
freeze each project at whatever the global happened to be the day it was
created — the opposite of what a default is for. Pin the model, change the
global turn budget next month, and the project follows.

- **The chain reaches the server, not just the toolbar.** Both doors to a new
interactive chat — the WebSocket `chat` frame and `POST /api/sessions` — used
to carry their own literal set (`sonnet` / `auto` / `single` / 30 turns). An
API client or a browser running a cached older build therefore created chats on
values nobody had configured, while the toolbar on screen showed the resolved
ones. Both now resolve the same chain, and the resolved row is computed before
the session is inserted so the stored row and the run cannot disagree.

- **Telegram and the scheduler deliberately do not inherit it.** They keep their
own 30-turn budget, now under a name (`UNATTENDED_MAX_TURNS`) rather than
copy-pasted at eleven call sites. A scheduled job that silently inherited
someone's `turns: 200` would spend a budget nobody was watching, and every
existing install would have jumped from 30 to 50 on upgrade without being
asked. Per-channel budgets, if they are ever wanted, belong in their own
setting — not in the dials that describe a chat window.

- **An empty turn-budget box no longer means 30.** The value the browser actually
put on the wire fell back to a literal that matched neither the box's own
placeholder nor the server's built-in. It now falls back to your configured
default.

### Remote projects get a file browser

- **List a directory, open a file — over SSH.** The file tree, the file viewer
and `@`-mention search work on remote projects, which previously answered
"file browser not available". That refusal was correct while there was no
remote read path: the browser reads the local disk, so pointing it at
`/home/user/project` from a Windows client resolved to `C:\home\user\project`.

- **Read-only by design.** Download, share, copy-image and image/PDF preview stay
hidden for a remote file rather than failing under your finger, and come back
the moment you open a local one.

- **Caps are announced, never silent.** Listings stop at 2000 entries and files
at 2 MB, both enforced on the remote host — an 8 GB log never crosses the link
— and both stated in the UI instead of quietly truncating.

- **Three guard layers, none of them the local one.** Paths resolve with POSIX
semantics whatever this server runs on; the remote script re-checks containment
against the physical path (`pwd -P`), because a symlink inside the project
satisfies a textual check by construction; and every row whose path escapes the
base the remote echoed back is dropped. A symlink whose final component is a
file is refused rather than followed. Filenames are attacker-controlled in the
way that matters — a repo you cloned can contain one with a newline in it — so
every control line is framed by a per-request random nonce.

### Remote runs find the tools that are actually installed

- **`bash -lc` is a login shell, not an interactive one.** Every version manager
— mise, asdf, nvm, pyenv, rbenv, nodenv, fnm, sdkman — publishes its binaries
from `~/.bashrc`, and the stock Debian/Ubuntu `~/.bashrc` returns immediately
when not interactive. The symptom was a SessionEnd hook dying with
`node: not found` on a host where `which node` answers instantly. Remote runs
now build the PATH those managers would have set, after the `cd` rather than
before it, since mise and asdf pin a version per directory.

- **It cannot take the run down with it.** The prelude is chain-safe: the caller
joins it to the `claude` invocation with `&&`, so it never ends on a failed
test, and a user's own `$CCS_REMOTE_INIT` runs under `eval` so a syntax error
there stays a runtime failure instead of a parse error that swallows everything
behind it. It sources no rc file — that output is a stream-json pipe, and one
MOTD banner derails the parse.

- **The Test button reports it.** A connection test runs the same prelude and
says whether `node` and `claude` resolve, so a broken PATH shows up on the host
form instead of surfacing mid-turn as a hook failure that names the wrong
problem.

### Fixes

- The MCP task manager offered `haiku` / `sonnet` / `opus` in its model enum, so
an agent creating a task could not pick **Fable** — the fourth model the UI has
exposed since 7.4.

## 7.6.0

Bots stop being a web-only feature, and gain a mode where several of them talk to
Expand Down Expand Up @@ -102,6 +195,10 @@ remote writes, a remote poll loop, and somewhere for the agent's terminal to liv
agent with none of its prompt or model.
- The `/` command popup clamps each preview to two lines. A command's prompt can run to
thousands of characters, and one entry unrolled over the whole list.
- `AGENTS.md` is read when a project has no `CLAUDE.md`. The `claude` CLI discovers only
the latter, so a project standardised on `AGENTS.md` ran with none of its conventions,
silently. Precedence is exclusive — when a `CLAUDE.md` exists the other is not read at
all — and the instruction-file editor opens whichever one the run actually uses.

## 7.5.4

Expand Down
22 changes: 21 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,26 @@ is testable without booting anything:
- **Only a chat with no session of its own is seeded.** `applyChatDefaults()` runs from
`newTab()`, from `switchProject()` and at boot — always behind `if (!currentSessionId)`.
An existing chat carries its own mode/model in SQLite and `loadSess()` must keep winning.
- **The chain governs chat CREATION; execution channels keep their own budget.**
Both server-side doors to a new interactive chat resolve it via
`chatDefaultsForWorkdir()` (sessions have no `project_id` column, so the project is
matched by workdir the way `/api/activity` does): the WS `chat` frame and
`POST /api/sessions`. Both used to carry a private literal set — `sonnet`/`auto`/
`single`/`30` — so an API client or an older cached SPA created a chat on values
nobody had configured while the browser's toolbar showed the resolved ones. `_cd` is
hoisted ABOVE the `createSession` INSERT so the stored row and the run cannot disagree.
`chatDefaultsForWorkdir()` returns the FLAT `.effective` row, not the
`{effective, global, overridden}` envelope the REST endpoints hand the browser.
- **Telegram and the scheduler deliberately do NOT inherit it** — `UNATTENDED_MAX_TURNS`
(30, declared once next to the `chat-defaults` require). A scheduled job that silently
inherited someone's `turns: 200` would burn a budget nobody was watching, and every
existing install would have jumped 30 → 50 on upgrade without being asked. The same
constant names every task/chain creation default (REST, MCP `create_task`/`create_chain`,
Kanban dispatch), so a bare `30` reappearing next to a runner is a review signal. If
per-channel budgets are ever wanted they belong in their own setting, not in the chat
dials. The `maxTurns || 30` floors inside the shared runners are left alone on purpose:
they fire only when a caller passes nothing at all, which is a different concern from
a channel policy. Pinned by `test/chat-defaults-api.test.js`.

### Markdown Rendering in SPA
- During streaming: `renderStreaming()` handles unclosed code fences
Expand All @@ -351,7 +371,7 @@ is testable without booting anything:

## How to Verify Changes

`npm test` runs 63 test files under `test/` (18 `test/render/*.test.mjs` + 45 `test/*.test.js`). There is no CI yet, and nothing covers the live browser/WebSocket path, so also verify that manually:
`npm test` runs 63 test files under `test/` (18 `test/render/*.test.mjs` + 45 `test/*.test.js`), and `.github/workflows/ci.yml` runs the same command on every push and PR to `main`. Nothing covers the live browser/WebSocket path, so also verify that manually:

```bash
# 1. Start server
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,10 +417,14 @@ The active engine is always visible at a glance: a **⚡ Max** badge appears in

**Global default** — set your preferred engine once with the **★** button next to the toolbar selector (the star lights up when the current selection is your default). Every new chat — and every new **Kanban** card and **Scheduler** task — starts on it, while any individual chat or task can still override per-item. The engine selector lives in Chat, the Kanban task form, and the Scheduler form alike, and falls back to **API** automatically when `tmux` is unavailable (e.g. native Windows without WSL) — the Subscription option is disabled with a clear hint rather than failing only after you send.

**New-chat defaults** — the same idea, extended to the whole toolbar. Pin **mode, agent mode, model, effort and turn budget** in Settings → *New-chat defaults*, and every new chat opens on them instead of on the built-ins. A project can override any subset of the five in its own settings; it stores **only the dials you actually pinned**, so the rest keep following the global value as you change it later. The chain is **project override → global default → built-in**, and every row shows which of the three it came from, with a one-click Reset. Existing chats are never re-seeded — each one carries its own settings in SQLite.

### 🌐 Remote Access & SSH

**SSH** — add remote servers, create projects pointing to directories on them. Claude works there as if local. Type `#` in chat for quick multi-server attachment. Screenshots and files auto-upload via SFTP.

**Remote file browser** — the file tree, the file viewer and `@`-mention search now work on SSH projects as well, not only local ones. Read-only by design: list a directory, open a file. Download, share and image/PDF preview stay hidden for remote files instead of failing under your finger. Listings are capped at 2000 entries and files at 2 MB — both enforced on the remote host and announced in the UI, so an 8 GB log never crosses the link. A symlink pointing outside the project root is refused, not followed.

**Remote Access** — one click: cloudflared (no signup) or ngrok. Public HTTPS URL in seconds. Works behind NAT, firewalls, corporate VPNs. URL sent to Telegram automatically.

### 📊 Dashboard
Expand Down Expand Up @@ -485,12 +489,12 @@ npx github:Lexus2016/claude-code-studio # launch as usual
| **Modes** | Auto, Plan (read-only + Execute Plan), Task, auto mode switching |
| **Skills** | 30 built-in, auto-classification, plugin discovery, custom `.md` files |
| **Commands** | 10 built-in slash commands, custom commands |
| **Remote** | SSH servers, SFTP upload, `#` quick-attach, cloudflared/ngrok tunnels, remote CLI session import |
| **Remote** | SSH servers, SFTP upload, `#` quick-attach, cloudflared/ngrok tunnels, remote CLI session import, read-only remote file browser (tree + viewer + `@`-mention search over SSH, capped and symlink-guarded) |
| **Mobile** | Native-feel UI, bottom sheet, scroll-snap Kanban, iOS-safe, touch-optimized |
| **Dashboard** | Activity heatmap, tool usage, model distribution, Automation Index, peak hours |
| **Reliability** | security-hardened Telegram uploads, self-healing sessions, crash protection, atomic writes, instant stop, rate limit auto-wait, concurrency safety (session lock + busy_timeout), orphaned session lock auto-cleanup |
| **Workspace** | Global view across every project at once — one search over tasks and files, jump straight to the owning project |
| **Settings** | In-app settings form, every value shown next to the source it comes from (shell env / `.env` / local `config.json` / global / project override / built-in default), override + shadow + ignored badges, secrets masked server-side, one-click reset to default, raw-file editors alongside |
| **Settings** | In-app settings form, every value shown next to the source it comes from (shell env / `.env` / local `config.json` / global / project override / built-in default), override + shadow + ignored badges, secrets masked server-side, one-click reset to default, raw-file editors alongside, global new-chat defaults (mode / agent / model / effort / turns) with sparse per-project overrides |
| **Security** | bcrypt auth, AES-256-GCM SSH, Helmet.js, path traversal protection, XSS/SQLi prevention |
| **Platform** | Windows/macOS/Linux, Docker (non-root, registry mirror), LLM proxy/gateway, 5 languages (EN/UA/RU/FR/HE), OpenRouter support |

Expand Down
6 changes: 3 additions & 3 deletions mcp-task-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const TOOLS = [
context: {
description: 'Curated context to pass to the child task. Include ONLY what the child needs — issue details, code snippets, error messages, etc. Can be a string or a JSON object. The child reads this via get_current_task.',
},
model: { type: 'string', enum: ['haiku', 'sonnet', 'opus'], description: 'AI model for the task (default: inherit from current task)' },
model: { type: 'string', enum: ['haiku', 'sonnet', 'opus', 'fable'], description: 'AI model for the task (default: inherit from current task)' },
depends_on: { type: 'array', items: { type: 'string' }, description: 'Task IDs that must complete before this task starts' },
chain_id: { type: 'string', description: 'Add this task to an existing chain' },
scheduled_at: { type: 'string', description: 'ISO 8601 datetime for delayed execution' },
Expand All @@ -70,15 +70,15 @@ const TOOLS = [
title: { type: 'string' },
description: { type: 'string' },
context: { description: 'Context for this specific task' },
model: { type: 'string', enum: ['haiku', 'sonnet', 'opus'] },
model: { type: 'string', enum: ['haiku', 'sonnet', 'opus', 'fable'] },
max_turns: { type: 'number' },
depends_on_index: { type: 'array', items: { type: 'number' }, description: 'Indices (0-based) of tasks in this chain that must complete first. If omitted, depends on previous task.' },
},
required: ['title'],
},
description: 'Array of task definitions (max 10)',
},
model: { type: 'string', enum: ['haiku', 'sonnet', 'opus'], description: 'Default model for all tasks' },
model: { type: 'string', enum: ['haiku', 'sonnet', 'opus', 'fable'], description: 'Default model for all tasks' },
scheduled_at: { type: 'string', description: 'ISO 8601 datetime for delayed execution' },
recurrence: { type: 'string', pattern: '^(hourly|daily|weekly|monthly|every:[1-9][0-9]*:(hour|day|week|month)|times:[1-9][0-9]*:month)$', description: 'Repeat schedule. A preset (hourly|daily|weekly|monthly), or "every:N:unit" where unit = hour|day|week|month (e.g. "every:2:hour", "every:10:day", "every:6:month"), or "times:N:month" = N runs per calendar month (e.g. "times:3:month").' },
recurrence_end_at: { type: 'string', description: 'ISO 8601 datetime to stop recurring' },
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "claude-code-studio",
"version": "7.6.0",
"version": "7.7.0",
"description": "Full-featured web workspace for Claude Code — chat, Kanban, multi-agent, MCP, skills, projects",
"bin": {
"claude-code-studio": "./bin/cli.js"
Expand Down
6 changes: 5 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11613,7 +11613,11 @@ <h2 id="dlgModalTitle" data-i18n="dlg.title">Делегувати агенту</
// send the API path (claude -p) regardless of any saved/default engine. Restore: engine: curEngine,
engine: 'api',
effort: curEffort || undefined,
maxTurns: parseInt($i('maxTurns').value) || 30,
// An empty turns box must fall back to the user's CONFIGURED budget, not to a
// private literal: this is the value that actually goes on the wire, and 30 was
// neither the toolbar's seed nor the server's built-in. `_chatDefaults` holds the
// resolved project > global > built-in chain fetched at boot.
maxTurns: parseInt($i('maxTurns').value) || (_chatDefaults?.effective?.turns || 50),
workdir: curWorkdir || undefined,
}));
}
Expand Down
Loading
Loading