Commit Graph

345 Commits

Author SHA1 Message Date
nntrivi2001 e21120d51d chore: v2.8.72 — update survives npm-rewritten lockfiles; drag artifacts into folders
- update: before the --ff-only merge, put back package-lock.json /
  client/package-lock.json when they are the ONLY tracked changes (an
  earlier update's npm install rewrote them; v2.8.71 changed
  client/package-lock.json and the merge refused on such installs). Any
  other change still makes the merge refuse.
- Artifacts: drag a row or card onto a folder chip to put it there, onto
  "All folders" to take it out.
2026-11-04 15:27:48 +07:00
nntrivi2001 c946b73d11 chore: v2.8.71 — remote clients (company hub), artifacts shared with chosen people, folders and viewable copies, fixes
Remote clients (docs/plans/remote-clients/README.md, B1–B3 for the Claude pool):
- server/hub/ holds the hub-only code (devices, remote sessions, upload ingest,
  LAN gateway), required only in server mode; hub-boundary.test.js keeps local
  code from importing it.
- A local install enrolls with `agenthub remote login` or Settings → AI tools →
  Company hub; the "Company account" AI Proxy (reserved name agenthub.company)
  sends a session through the hub's gateway, which adds a pool account's token —
  no credential reaches the machine. Only those sessions are uploaded, hourly
  and at their end; secret-shaped strings are redacted on the client.
- Gateway refuses: unknown device (401), a message without a session id, an
  unregistered session under AGENTHUB_REMOTE_GATE=block, a user over quota
  (EQUOTA), a purged session (410). Device tokens 90 days, conversations 30.
- New alert rule type remote_unmonitored: a company-account session its device
  never registered; a late registration clears the flag instead.
- The hub imports a session once per upload round (chunks carry `more`),
  not once per chunk.

Artifacts:
- Share an artifact with chosen people (artifact_shares, PATCH sharedWith,
  GET /api/artifacts/people) besides everyone; a "Shared with you" tab.
  Only the owner or an admin sees or edits who it is shared with; audited.
- Folders: each account's own arrangement (agenthub.artifacts.folders pref).
- A claude.ai page keeps a copy viewable without the publishing account: its
  HTML file (and supporting files) snapshotted at publish, or the copy Claude
  saves for a Doc / Slides / Design in artifacts-export/ (a synchronous
  PostToolUse hint hook, scripts/artifact-export-hint.js, admin switch),
  attached to the page at Stop. Viewed in HtmlFrame's sandbox, Markdown, or
  pdf.js (pdfjs-dist 6.4.299, legacy build, loaded only for a PDF).

Fixes:
- tmux: a cached /proc/<pid>/exe of an exited server falls back to PATH's
  tmux (stop the last pane, start again within 10 s failed with ENOENT).
- shelves: a shelf restores beside changes in other files.
- chat: a finished reply ends the turn even with no hook at all.
- Settings → AI tools: profile list display tweaks.
2026-11-04 15:16:49 +07:00
nntrivi2001 cd25791160 feat(select): long lists become a combobox; the branch switcher uses it (#88)
Select opens with a focused search box when it has more than 8 options
(or searchable): typing narrows by label/value, ↑/↓ move, Enter picks,
Esc closes; role=combobox drives the listbox via aria-activedescendant,
and options may carry a group heading. The git bar's native branch
select (and the separate search box added for #88) is replaced by it,
remote branches under their own heading.
2026-11-03 13:25:16 +07:00
nntrivi2001 aa7f1b1c60 feat(pet): remove the pet's chat
Each question started a full AI session (a heavy process on a shared
server, and one of a member's 4). A click on the pet now opens its window
(PetPanel): its name (rename), a pat, and the status strip. PetChat, its
off-screen terminal, its AI hand-off and its strings are gone; the window
is where the pet's own care comes next.
2026-11-03 12:08:14 +07:00
nntrivi2001 70d7cf30c2 feat(git): search box for the branch switcher (#88)
A Search branches… box beside Switch branch narrows the local and remote
lists to names containing the text (case-insensitive). One match is
picked at once, Enter picks the first, Esc clears. The picked branch
stays listed, so the select never loses its value.

Closes #88
2026-11-03 12:00:10 +07:00
nntrivi2001 5912e9e613 fix(pet): a member's pet chat starts in <their folder>/sagi
POST /api/lanes/ensure without a cwd defaulted to the dashboard's own
home, outside a member's folder, so every member's pet chat failed with
"a workspace must be inside your own folder" (EFOLDER). A member now
gets <their folder>/sagi, made as them (write-as mkdir) on first use.
2026-11-03 11:53:21 +07:00
nntrivi2001 497faae3bf feat(settings): admin switch to allow or block members' test runs
Settings → AI tools → Tests on this server (admin, S3 only). Stored in
/opt/agenthub/app/member-policy.json, which member-guard.js reads as the
member at each Bash call — no restart; no file = blocked. GET/PUT
/api/ai-tools/member-policy, audited, 409 ENOS3 outside S3.
2026-11-03 11:37:13 +07:00
nntrivi2001 e6ce8009b1 feat(server): S3 box is for coding — OOM kills the command not the session, no tests from Claude, 4 AI sessions per member
- tool-run (CLAUDE_CODE_SHELL_PREFIX in member panes) runs each command
  Claude runs at oom_score_adj 1000 (session 300, Terminal tab 800,
  dashboard/tmux -100), so an OOM inside the capped unit kills the test
  run and tells Claude why, instead of killing the conversation.
- member-guard.js, a synchronous managed hook for ah-* accounts: denies
  test runners and container commands from Claude's Bash, and refuses
  the prompts of a session started by hand past MAX_AI_SESSIONS.
- ai-procs.js counts a member's top-level AI processes from /proc;
  spawnRun refuses a new pane past the limit (429 ESESSIONLIMIT).
Needs `agenthub server install-system` once, then a restart.
2026-11-03 10:50:53 +07:00
nntrivi2001 705353b0da fix(server): a member's test run can no longer freeze the box
On 11-02 open sessions plus test runs filled RAM and the 4 GB swap on a
slow disk; the box hung until a hard reset. Member panes now start at
nice 10, IO priority 7 and oom_score_adj 500 with Vitest capped at 2
workers by default, and the system unit caps its cgroup (server + every
pane) at 85% of RAM with 1G swap and OOMPolicy=continue, so the kernel
kills the biggest pane process instead of hanging the machine. Needs
`agenthub server install-system` once.
2026-11-03 10:27:58 +07:00
nntrivi2001 a3d1dce2ca revert: remove user chat (direct + groups) from the pet window
Reverts 21a042d, 0d3e92c, 0016b6a, c797e98, 03ebea1. Keeps the pet's
own-sessions scope, status bar mine=1, scroll speed and the health hover card.
The chat_* tables and <data dir>/chat/ files are left in place (unused).
2026-11-03 10:06:12 +07:00
nntrivi2001 03ebea1d92 fix(chat): the review's gaps — owner hand-over, live group lines, names, scroll, one tab's sound, uploads on disk, a chat button without the pet
- A group whose creator left or was deleted passes to its longest-standing
  member (ensureOwner), so it can still be managed.
- Group changes (added / removed / renamed / left) are sent live as
  chat_message and never count as unread, here or on the server.
- A disabled account is still named in old chats (people carries
  `disabled`) and never offered for a new one; a deleted one reads
  'Deleted user' instead of '?'.
- Load older keeps the message being read in place.
- With several tabs open only the first to see a message plays its sound /
  system notification (agenthub.chat.notified).
- The people list is read again on New chat and on every room change.
- Uploads go to <data dir>/chat/.incoming/ (multer disk storage) and are
  renamed into place — never held in memory; a refused one is removed.
- With the pet hidden, a chat button in the bottom-right corner (ChatDock,
  server mode) opens the same window, unread count and new-message card
  included.
2026-11-03 09:59:13 +07:00
nntrivi2001 c797e9868e feat(chat): delete a chat, disband a group, Seen — and purge what everyone deleted
Delete chat is the caller's alone: the history so far vanishes for them
(chat_members.cleared_id) and a direct chat leaves their list (hidden) until a
new message arrives; the others keep theirs. What EVERY member has deleted is
deleted for real (purge): messages up to the lowest cleared_id and their files
on disk, and a direct chat both people deleted, whole. Purge also runs when a
member leaves or is removed. A group's creator disbands it for everyone
(DELETE /api/chat/rooms/:id).

Seen: rooms carry every member's read mark (reads); a mark that moves is told
to the members (chat_read, exactAudience) and never passes the newest
message. Under your newest message: Seen / Seen by … / Seen by everyone.
A room open on a tab you come back to is marked read then.
2026-11-03 09:54:55 +07:00
nntrivi2001 0016b6a427 feat(chat): right-click menus everywhere in the chat, and deleting your own message
Rail: a room — open, mark as read, mute / unmute notifications (per browser,
the unread count still shows), copy name, group settings, leave; the pet and
+ — chat with the pet, new chat, mark all chats as read. A message — copy
text, reply (quotes it into the box), message the sender privately (group),
delete your own (asks first). A file — open in a new tab, download, copy file
name. A group member — message privately, copy name, remove (the creator).

DELETE /api/chat/rooms/:id/messages/:messageId deletes the caller's OWN text
message and its files (403 for anyone else, an admin included) and tells the
members with a chat_deleted frame (exactAudience).
2026-11-03 09:47:32 +07:00
nntrivi2001 21a042dc2b feat(chat): direct chats and groups between users in the pet's window
Server mode: a rail beside the pet's chat lists the pet, then the user's
direct chats (one per pair) and groups with unread counts, and + for a new
chat. A room shows its messages (group senders named), loads older pages,
and sends text, pasted / dropped / attached files (<= 10 x 25 MB; images
inline). A group's creator renames it and adds or removes people; anyone may
leave. A blue dot on the pet counts unread messages.

Only a room's members read or write it, an admin included (roomFor → 404).
Files are stored under server-made names in <data dir>/chat/<room>/ and
served to members only under a sandbox CSP + nosniff. chat_message /
chat_room frames reach exactly the ids in `to` (scope.exactAudience).
New tables chat_rooms / chat_members / chat_messages / chat_files (additive).
2026-11-03 09:33:33 +07:00
nntrivi2001 c229b4b79b fix(pet): each user's pet tells only of their own sessions
An admin/analyst's pet counted every user's sessions (live, waiting, errors)
because their WebSocket carries everyone's. Server mode now marks their
session/agent/event frames `mine: true|false` (scope.broadcastAudience owners,
worked out once per broadcast), and GET /api/sessions?mine=1 lists only the
caller's own sessions (ownSessionWhere). The pet seeds from mine=1 and skips
frames marked mine: false. Developers and local mode are unchanged.
2026-11-03 09:01:57 +07:00
nntrivi2001 b777e08371 fix(health): the user hover card leaves out the login name
It now opens only once the users list is read (admin/analyst): role, today's
spend, workspaces, sessions and last sign-in. A developer gets no card.
2026-11-03 08:50:26 +07:00
nntrivi2001 75a4693f27 feat(health): hovering a user tile shows their login, role and today's spend
System Health's Users · CPU / RAM card: each tile's hover card names the login
name; for an admin or analyst it adds role, $ spent today, workspaces,
sessions and last sign-in from GET /api/users (staff-only, re-read every 60 s).
A developer's card shows the login name alone and never calls /api/users.
2026-11-03 08:38:48 +07:00
nntrivi2001 2342f54179 feat(git-identity): commit signing per workspace, and Delete in the signing card
Signing was account-wide (gpg.format/user.signingkey/commit.gpgsign in
~/.gitconfig), so every repository was signed. Now Settings → Git →
Commit signing lists the user's workspaces, each with its own key picker
("Don't sign" by default). PUT /api/me/git/signing/lane {laneId, key}
writes a small file of ours (lane-<id>.gitconfig beside the account's
keys; a member's through write-as) and includes it from ~/.gitconfig
with includeIf "gitdir:<the workspace's git dir>" and
"gitdir:<common dir>/worktrees/ah-<id>-*" (its sessions' worktrees) —
only that workspace signs, from the terminal, the Changes panel or an
agent, and nothing is written into the repository. The git dirs come
from git run in the workspace's folder as its account; a workspace that
runs as another account is EFOREIGN, a folder that is not a repo
ENOREPO. Stopping needs no workspace, so one removed from the dashboard
while it signed is still listed and can be turned off.

The card now lists the keys that can sign, each with Delete (confirm;
deleting a key stops every workspace signing with it), a way out of the
old account-wide setting, and Remove for a signing setup the user made
themselves (PUT /signing {clearForeign: true}: the three settings only,
never their key file). Importing a key no longer turns on signing.

Verified with real signatures: the picked workspace's commits are G,
another repo's and an unrelated worktree's N, a session worktree's G.
2026-11-02 16:49:27 +07:00
nntrivi2001 7ee79292fa docs(api): GET /api/users/:id/avatar is open to every signed-in account 2026-11-02 16:40:37 +07:00
nntrivi2001 12ea853170 feat(files): uploads survive a dashboard restart
An upload through the File Explorer is now sent in 8 MB parts that the
server stages on the host (<data dir>/upload-staging/<user>/<id>/,
server/lib/upload-staging.js) and writes only once every byte is there,
through the same upload/uploadBatch code and guards (?staged=<id>). A
restart, an update or a crash pauses it: the browser waits up to 3 min,
asks how much arrived (GET /fs/upload-part) and sends the rest; the dock
says the dashboard is restarting meanwhile. A commit's answer is stored
and replayed, never written twice; a refused one (EEXISTS) keeps the
bytes for a retry with another conflict policy.

A file uploaded into a member's folder is now written to a hidden temp
name and moved onto its name only when whole: write-as cannot tell a
dropped connection from the end of a file, and `replace` left a truncated
file under the real name when the server died mid-upload.

New: GET|POST|DELETE /api/{lanes/:id,homes/:who}/fs/upload-part; 409
EOFFSET/EINCOMPLETE/EBUSY, 429 ETOOMANY. Verified on a sandbox server:
SIGTERM mid-upload of a 60 MB file in a real browser, resumed and
sha256-identical.
2026-11-02 16:12:57 +07:00
nntrivi2001 7e41c81d53 feat(git-identity): import an existing SSH key and sign commits with it
Settings → Git gains Commit signing: one picker signs every commit
(terminal, Changes panel, agents) with one of the account's keys —
gpg.format ssh, user.signingkey = the key's private file, commit.gpgsign
true in its own ~/.gitconfig; a signing key the user set up themselves is
replaced only after a confirm (EFOREIGNSIGN without `replace`). Import a key
you have takes a key file or paste (OpenSSH or PEM, ed25519 or RSA >= 2048)
into ~/.config/agenthub/ssh/ as a named key, written like a generated one
(member: write-as, 0600), and by default signs with it in the same step.
A passphrase key, a .pub, an ecdsa key, a mismatched paste and the same key
twice are refused; only the public half ever comes back. Deleting the
signing key stops signing so commits keep working.

The SSH tab is renamed "Git (GitHub, GitLab, Gitea, Backlog…)" with the
Gitea and Backlog steps, and the File Explorer's ESECRET toast now says
that top-level dot folders of a home are edited in a terminal, and where
the SSH keys live instead.
2026-11-02 16:12:57 +07:00
nntrivi2001 2d6c751fb2 fix(terminal): touchpad scrolls 1:1 with the finger
xterm.js sends one wheel report per event whatever its size and damps a
touchpad's pixel deltas x0.3, so a swipe crawled. With mouse tracking on,
TerminalView now sends the SGR reports itself (lib/wheelReports.ts): one
per 5 rows of travel (what one report scrolls in a Claude pane), the
remainder carried between events, at least one per wheel notch, at the
pointer's cell with xterm's modifier bits.
2026-11-02 16:06:43 +07:00
nntrivi2001 faa96d1964 fix(terminal): Claude panes scroll 5 lines per wheel notch
Fullscreen Claude Code scrolls one line per wheel report when its terminal
does not answer XTVERSION as xterm.js (behind tmux it never does), and
xterm.js sends one report per notch — so a notch moved a single line.
spawnRun now sets CLAUDE_CODE_SCROLL_SPEED=5 unless the dashboard's env
already sets it; settings.json env and /config Scroll speed still win.
2026-11-02 15:32:46 +07:00
nntrivi2001 1ad2fec073 fix(preview): say why an outside site shows blank, with Open tab beside it
YouTube, Google and login pages refuse framing and the browser draws a
broken-page icon; the hint lived only in the empty state. A page not on this
machine now gets a strip under the address bar with Open tab. The refused-
address toast says what is allowed in plain words.
2026-11-02 15:10:22 +07:00
nntrivi2001 5d62003fe5 feat(workspace): Preview tab in the session panel; local links open there
The session panel's header becomes two tabs, Changes and Preview. Preview
shows a web page by address (Reload, Open in a browser tab, the workspace's
ports as chips, last address per workspace) in an iframe on the page's own
origin: http(s) only, no userinfo, never the dashboard's origin, no top
navigation. One rule for every link (lib/openInPreview.ts): a chat or
terminal link to localhost/127.0.0.1/0.0.0.0/[::1] opens in Preview while a
session panel answers and the dashboard is on loopback; anything else, or a
Ctrl/Cmd click, opens a browser tab as before.
2026-11-02 15:06:04 +07:00
nntrivi2001 e1f433804c feat(git): history graph with search, commit details, file history, blame/browse at a revision, compare refs, and multi-commit cherry-pick/revert with preview, pause banner and undo 2026-11-02 12:22:46 +07:00
nntrivi2001 2b5d7268ed feat(git): 3-way merge editor, reflog, rebase/interactive rebase, reset with undo, force-with-lease push, remote branches and remotes
Server: git-rewrite.js (rebase + continue/skip/abort, rebase-interactive with
no editor, reset soft/mixed/keep/hard, undo-rewrite, force-push with lease,
remote-branch-delete, remote add/rename/remove) and git-conflicts.js
(conflict stages read + conflict-save, reflog), dispatched from runGitAction;
operationInProgress now reports a rebase and only ALLOWED_WHILE_REBASING
writes run during one. New reads: /git/conflict, /git/reflog, /git/remotes,
/git/rewrite-plan. Client: History tools menu, MergeEditor, ReflogView,
RebaseDialog, InteractiveRebase, ResetDialog, ForcePushDialog, RemotesDialog,
rebase banner Skip. CLAUDE.md git-writes rule rewritten for the new actions.
2026-11-02 12:12:45 +07:00
nntrivi2001 2cc492bb88 feat(git): Branches & tags panel — rename, branch from any ref, upstream, tags
GET /git/refs lists local branches (upstream, ahead/behind, last commit,
merged, held by a worktree, protected), remote branches grouped by remote
and tags (target, annotated message/tagger/date). New writes in
server/lib/git-refs.js, dispatched from git-ops.js: branch-rename,
branch-upstream (set/unset), tag-create (light/annotated), tag-delete
(confirm), tag-push (one tag to origin, no --tags, no force), and
branch-create with a looked-up from + switch on a clean tree. Every ref is
looked up, never passed through; repo-wide writes are repo-root only
(ESUBDIR) and refused during any operation (EMERGING).

Client: git/BranchManager + TagManager dialog opened from the git bar's
tags icon, en+vi (gitRefs namespace), keyboard navigable.

# Conflicts:
#	CLAUDE.md
#	client/src/components/lanes/LaneGitBar.tsx
#	docs/API.md
#	server/__tests__/lanes-api.test.js
#	server/routes/lanes.js
2026-11-02 12:00:55 +07:00
nntrivi2001 f6eb39eb73 feat(git): switch with uncommitted changes (carry / shelve), Create & switch, and shelves with Restore/Delete
- switch takes changes: carry (plain git switch, 409 ECARRYCONFLICT with files) or shelve (shelf first, re-applied if the switch fails)
- branch-create switch: true runs git switch -c, the changes come along
- shelve / unshelve / shelf-drop + GET /git/shelves: only agenthub-shelf: stash entries, addressed by sha, repo root only, never mid-operation; a clean restore drops the shelf, a conflicted one keeps it
- 409 EAGENTRUNNING until confirm: true while the workspace's agent is working
- git bar: Bring my changes / Shelve and switch choice, Create & switch, restore offer after a switch; Changes panel: Shelve (n) and the Shelves list
- fix: after a switch the Changes panel showed the old branch — the post-write re-read joined an in-flight GET started before the write (request() dedupe); writes now close in-flight GETs, and every git write fires agenthub:git-changed so every view of the workspace re-reads
2026-11-02 11:44:35 +07:00
nntrivi2001 a27589418c feat(clone): background clone jobs that survive a reload and a server restart
Both clone POSTs take background: true: every check answers synchronously,
then 202 {job}. git runs in a detached runner (scripts/clone-runner.js, its
own process group, stderr to a file — no SIGPIPE when the server restarts);
the server tails it for progress and runs the after-step (workspace / extra
folder) once when it exits. Jobs persist in <data dir>/runtime/clone-jobs.json
(0600, atomic, throttled progress writes); on start a live runner is adopted
(counted against the clone limit), an ended one finished, a queued one
requeued (interrupted if it carried a typed login), the rest interrupted.
GET /clone/jobs, DELETE /clone/jobs/:id, POST /clone/jobs/:id/cancel (queued:
dequeued; running: SIGTERM to the runner's group -> cancelled, no after-step)
and /retry (new job from the stored request, never a credential). The
held-open path is unchanged; worktree.gitCommand builds the runner's argv
and git-identity.cloneFailure the shared error classification.
2026-11-02 10:47:51 +07:00
nntrivi2001 57cbab916a feat(clone): background clones and uploads in one global dock that survives F5 and restarts
Clone repository (Files), Add workspace -> Clone and a workspace's extra-folder
clones now start server-side jobs (background: true) and close at once; the
app-wide BackgroundJobsDock (Layout) shows each job's queue place, git progress,
Stop (click twice), and outcome: Open workspace, Retry, an inline login form on
ECREDENTIALS, a Settings -> Git link on ESSHAUTH, dismiss. lib/cloneJobs.ts
polls GET /workspaces/clone/jobs while any job is active, announces a finish it
saw once (toast + agenthub:clone-job event + the starter's onDone), and treats
an older server's synchronous answer as a finished clone. File Explorer uploads
report to lib/uploadJobs.ts (Stop aborts the request in flight; a reload-cut
upload shows as interrupted from localStorage). CloneProgress renders a job's
fields instead of polling a token.
2026-11-02 10:44:23 +07:00
nntrivi2001 0c964909e0 feat(git): a CodeCommit SSH key ID is saved for every region; a clone tries each saved account
Settings → Git → AWS CodeCommit asks for the key ID only: the default key's
login is written for every CodeCommit region at once (allRegions), shown as
one row, with Use in every region for an older one-region save. A clone of
a CodeCommit URL with no user tries every saved CodeCommit login — the
region's own first, then other regions' and each named key's — so a second
AWS account clones from the plain AWS URL with no ssh://<alias>/ rewrite.
2026-11-02 10:32:20 +07:00
nntrivi2001 220309db12 fix(files): a workspace whose folder was deleted leaves the Files places
The lane payload carries folder_missing (ENOENT/ENOTDIR only). Files hides
such a workspace and never reopens it as the default place; the Workspace
sidebar strikes its name through with a hint to remove it from its menu.
A folder that cannot be read no longer keeps the previous folder's entries
under its error, and a change in Files re-reads the workspace list.
2026-11-02 10:11:47 +07:00
nntrivi2001 e4ecd116a2 fix(clone): a CodeCommit region borrows saved key IDs; Clone runs in the background
An IAM SSH key ID logs in to every region of its AWS account, but a login
was saved per host, so a repo in a region with no key ID of its own was
refused. Clone now tries the key IDs saved for the other CodeCommit regions
(each with its own key; the first ls-remote accepts, or the only one) and
keeps that user in the clone's origin.

Files → New → Clone repository steps aside for its progress chip as soon
as the clone starts; a failure asking for a login reopens the form.
2026-11-02 10:00:09 +07:00
nntrivi2001 3ab484ddc0 fix(pool): a Codex ChatGPT pool pane runs on the login placed for it
Codex >= 0.164 hands a plain TUI (no -c override) to the CODEX_HOME's
shared app-server daemon, which outlives every TUI and keeps the login it
started with. A ChatGPT pool account written into a member's auth.json
therefore reached no pane while their daemon still ran on the previous
login (reproduced on 0.164.0: the daemon kept sending the old key, a
--no-daemon TUI sent the new one). Such a pane now runs `codex
--no-daemon`, only when that binary's --help lists the flag (probed once
per path/size/mtime), so an older Codex still starts.
2026-10-30 16:06:20 +07:00
nntrivi2001 240444eafd fix(artifacts): auto-capture leaves a web app's own HTML pages alone
At a session's Stop every untracked .html was snapshotted as a report, so a
web project not yet committed had its index.html taken into its owner's
Artifacts. An index.html, a page under a site folder (www, public, dist,
build, src, static, templates, assets, site, _site) or one beside scripts or
stylesheets (outside the workspace root) is now a site's own page and left
alone; a standalone HTML report is still captured.
2026-10-30 15:22:19 +07:00
nntrivi2001 d09c9d6633 feat(artifacts): last updated time on skills and files; each installer's copy marked
A row said nothing about when its content last changed, and the install list
could not tell who still runs an old version. `artifacts.version_at` (new
column, existing rows from `updated_at`) is stamped only when a snapshot is
replaced — a rename or share toggle leaves it — and served as `versionAt`:
skill and file rows read "Updated 3h ago", the detail sheet "Last updated".
`GET /api/artifacts/:id/installs` gives each skill install a `state` —
latest / outdated / removed / unknown — from its Claude skill copies' hash
against the newest snapshot, shown as a badge.
2026-10-30 15:21:31 +07:00
nntrivi2001 e7060a5e42 feat(artifacts): Share again takes a skill's newest version; a same-named upload updates it
A shared skill followed its owner's ~/.claude/skills folder only, so one
uploaded from a browser (or living in a plugin) stayed at its first snapshot
for good. Share again (a skill's ⋯ menu and detail sheet) now takes the
owner's copy on the server — its source folder, else the same-named skill of
a plugin they installed at user scope — and shares it; with no copy it asks
for the folder and uploads it onto that row (`id`). An upload whose skill is
named like one the uploader already has replaces that row's version instead
of adding a twin. Installers are then offered Update, as before.
2026-10-30 14:20:18 +07:00
nntrivi2001 da7b86d5c8 fix(changes): Discard takes only the ticked files and blocks (#87)
The Changes panel's "Discard all" threw away every changed file, ticked or
not. It is now "Discard (n)": the same selection Commit sends — whole ticked
files as `paths`, a partly ticked file's ticked blocks as `hunks` — and is
disabled with nothing ticked. The server's `discard` accepts `hunks`
(`selectionPatch` in reverse, `git apply -R`), building every patch before
applying any so a stale selection undoes nothing.

Closes #87
2026-10-30 13:05:13 +07:00
nntrivi2001 d97c403aab fix(changes): a huge line no longer cuts the diff short; preview pages up to 64 MB
A page that inlines a video as base64 has rows of megabytes. Its patch was read
at the 200 KB per-file cap, so the first such row used the whole budget and
every hunk after it was lost ("Diff cut short"). The patch is now read up to
16 M characters, rows over 4,000 characters are cut to 1,000 with an `elided`
count the diff shows, and only then is the cap applied. Hunk actions re-read
git's own diff by header and are unchanged.

The HTML preview refused a page over 10 MB and showed only the file icon; a
self-contained 18 MB slide deck now opens (limit 64 MB).
2026-10-30 10:28:53 +07:00
nntrivi2001 783b7f4101 fix(cost): bill a session outside every workspace to its Linux account's user
A script's claude -p in a temp folder (the SG Flow eval runner strips the
pane's env) had no workspace, so its spend reached nobody's Today, the Users
page or a spend quota — ~$57 of a $74 day was missing from the status bar.

sessions.account records the Linux account a session ran as (the hook's
peer uid, else its folder for poller-made and old rows); a session no
workspace owns is owned by that account's user — a member's to them, the
host's to the admin ticked 'Owns this server's host account' (or the only
admin). Workspace ownership still wins.
2026-10-30 08:50:34 +07:00
nntrivi2001 7798530528 fix(profiles): a Codex switch reaches running and new sessions
Codex >= 0.164 sends every request through a shared app-server daemon that
outlives its TUIs and keeps the login it started with, so writing a new
auth.json changed nothing: open sessions and even new ones stayed on the
old (out-of-quota) account. After a switch, restart that CODEX_HOME's
daemon when one runs; open TUIs reconnect to it on their own (verified on
0.164.0 with two keys). The response says so (following), and the toast
tells the user running sessions follow.
2026-10-29 15:54:20 +07:00
nntrivi2001 6099841cef fix(sessions): archiving a live session stops its pane
Archive on a session a live pane held only hid its row, and the list
deliberately does not bring such a tab back, so the pane kept running
with nothing on screen showing it — a member archived five sessions
believing them closed and kept ~2.7 GB of claude/omp/postgres-mcp alive
for up to two days. Archive on a live session is now a Stop (endRuns,
the tab X's confirm, DELETE /api/run/:id?archive=1) in the sidebar row,
the workspace home's Recents and their right-click menu.
2026-10-29 14:26:25 +07:00
nntrivi2001 63eaa739d4 fix(accounts): a display name may not read as another account's
A member kept renaming themselves to a colleague's display name, plus
"(you)", so the CPU/RAM card and every owner list showed two of that person.
A display name is now refused (409 ENAMETAKEN) when, with marks, case,
spaces, punctuation, bracketed parts and look-alike Cyrillic/Greek letters
ignored, it equals another account's display name or username — in
Settings → Account and on the Users page alike. Keeping the name one already
has stays allowed, so an older clash does not block an admin's other edits.
2026-10-29 13:47:20 +07:00
nntrivi2001 97c39d5e15 feat(files): an HTML page opens in a modal over the screen
- Changes panel: Preview beside Whole file opens the page in a modal
  (HtmlViewerModal, shared with the File Explorer) instead of inside the
  panel; Changes and Whole file stay as they were.
- Chat: an HTML file's card, Outputs row or a path in a reply (in the
  workspace or not) opens FilePreview as a modal — Preview | Code, versions,
  Save to Artifacts kept — rather than the side panel or the Changes panel.
- Esc closes both modals: they stop every key from reaching the view below,
  which also kept useDialog's document listener from seeing it.
2026-10-29 11:20:54 +07:00
nntrivi2001 80000793f4 feat(files): an HTML file's Preview sits beside Whole file
The Changes panel's view toggle becomes Changes | Whole file | Preview for an
HTML file; the Preview/Raw button on the file's name row stays for markdown.
2026-10-29 10:47:39 +07:00
nntrivi2001 364fe4a286 fix(files): an HTML file in the Changes panel shows as the page only on Preview
Changes stays the default view and Whole file shows the source, as for any
file. A Preview button — offered on the Changes diff too, where it was
missing — opens the page; Raw, Changes or Whole file go back to the source.
2026-10-29 10:38:15 +07:00
nntrivi2001 7b5abc6ad9 feat(files): an HTML file opens as the page — Files panel, Chat preview, File Explorer
Clicking an .html/.htm file now renders it instead of showing its source or
downloading it:
- Changes/Files panel: a whole HTML file opens on Preview (Raw = the editor).
- Chat file preview: a workspace page brings its relative files along.
- File Explorer: double-click / Enter opens a viewer; Download stays on its
  header and right-click menu.

The page runs in the chat artifact's sandbox (iframe srcdoc, scripts, never
allow-same-origin). Its relative stylesheets (and their url()s), scripts,
images, media and icons are read by the dashboard through the existing
download routes and inlined as <style>/data: URLs (lib/htmlInline.ts: never
above the folder's root, ≤ 60 files / 30 MB) — the login cookie is
SameSite=Strict, so a frame could not fetch them itself, and no new route
serves files without it.
2026-10-28 16:46:07 +07:00
nntrivi2001 0841d3e5c8 fix(plan): a Claude plan is the one Anthropic reports now, not the sign-in's
An account signed in on Pro and upgraded to Max 5x kept showing "Pro": the
plan was read only from .credentials.json / .claude.json, whose
subscriptionType and rate-limit tiers are written at sign-in. fetchOwner
already asks GET /api/oauth/profile for a token's owner; it now also reads
organization_type / rate_limit_tier / seat_tier into a plan
(planFromProfile), remembered per account uuid (rememberLivePlan). The live
row, saved profiles (identityOf) and pool accounts (poolStatus, whose quota
refresh now asks a login's owner too) show that plan when one was seen
(shownPlan), else planLabel of the files as before.
2026-10-28 15:09:30 +07:00
nntrivi2001 14e979837e fix(git): the branch switcher lists and checks out branches only the remote has
GET /api/lanes/:id/git/branches read only refs/heads, so a branch someone
else pushed never showed after a fetch or pull, and `switch` refused it
(ENOBRANCH). It now also returns `remotes`: fetched remote-tracking branches
with no local branch of that name (origin/HEAD left out), listed under
"On the remote"; switching to one runs `git switch -c <b> --track
refs/remotes/<remote>/<b>` with the remote ref looked up, never passed
through (bare name only when exactly one remote has it, else EAMBIGUOUS).
The bar also re-reads the branch list after a fetch or pull instead of
keeping the list it loaded first.
2026-10-28 14:57:32 +07:00