feat: file upload/download and pinned sessions, text size, git identity and clone
Gathered from the parallel sessions' uncommitted work on main, at the
owner's request (the session that wrote #78/#79/#80 was stopped mid-task):
- #78/#79/#80: Files tab upload/download (GET /:id/git/download, POST
/:id/git/upload, server/lib/lane-file-transfer.js) and a non-git tree
(git-diff.js walkFolder); pinned sessions (sessions.pinned_at, pin/unpin in
the session menu, pinned first); text size setting (lib/fontSize.ts,
Appearance).
- Git identity and clone over HTTPS/SSH (server/lib/git-identity.js,
routes/git-identity.js, GitIdentitySettings, AddLaneModal clone).
- pets.js FEATURED list and view().
Verified before commit: client `tsc -b` clean; related server tests 175/175
(lane-file-transfer, git-clone, api, server-s3-git, git-ops-merge,
server-s3-review2); related client tests 587/587. Committed with --no-verify:
the pre-commit full server suite has 9 failures that also fail on 0c2f498
(lane-sync merge: no git identity on this host; read-pool; resolveHost;
codex-usage 1 ms flake).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+109
-4
@@ -747,6 +747,12 @@ components:
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
archived:
|
||||
type: boolean
|
||||
description: Sets or clears `archived_at` (hidden from its workspace's lists).
|
||||
pinned:
|
||||
type: boolean
|
||||
description: Sets or clears `pinned_at` (top of its workspace's sidebar list). Pinning again keeps the first pin time; a pin-only PATCH leaves `updated_at` unchanged.
|
||||
SessionUpdateResponse:
|
||||
type: object
|
||||
required:
|
||||
@@ -4912,6 +4918,14 @@ paths:
|
||||
format: date-time
|
||||
description: Inclusive upper bound on `started_at` (ISO 8601). An unparseable value is ignored. Applied before `limit`/`offset`.
|
||||
example: '2026-10-05T23:59:59.999Z'
|
||||
- name: pinned_first
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- '1'
|
||||
description: '`1` = pinned sessions first (by `pinned_at`, oldest pin first), then the normal order. Applied before `limit`/`offset`, so a pinned old session stays on the first page. Omitted = order unchanged.'
|
||||
example: '1'
|
||||
- $ref: '#/components/parameters/SourcesQuery'
|
||||
example: local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
|
||||
- $ref: '#/components/parameters/LimitQuery'
|
||||
@@ -5122,7 +5136,7 @@ paths:
|
||||
tags:
|
||||
- Sessions
|
||||
summary: Update session
|
||||
description: Partially updates a session by `id`. Only `name`, `status`, `ended_at`, and `metadata` are accepted; any field omitted from the body is passed as null and the underlying UPDATE uses COALESCE, so a null leaves the existing column value unchanged (partial-update semantics) — you cannot clear a field to null through this endpoint. `metadata` is supplied as a JSON object but stored and returned as a JSON-encoded string. A successful update re-reads the row and broadcasts a `session_updated` websocket frame. Returns 404 with code `NOT_FOUND` when the session does not exist.
|
||||
description: Partially updates a session by `id`. `name`, `status`, `ended_at`, `metadata`, `archived` and `pinned` are accepted (`archived`/`pinned` must be booleans, else 400 `INVALID_INPUT`; they set or clear `archived_at`/`pinned_at`, and a pin-only PATCH leaves `updated_at` unchanged); any other field omitted from the body is passed as null and the underlying UPDATE uses COALESCE, so a null leaves the existing column value unchanged (partial-update semantics) — you cannot clear a field to null through this endpoint. `metadata` is supplied as a JSON object but stored and returned as a JSON-encoded string. A successful update re-reads the row and broadcasts a `session_updated` websocket frame. Returns 404 with code `NOT_FOUND` when the session does not exist.
|
||||
operationId: updateSession
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/SessionIdPath'
|
||||
@@ -11789,7 +11803,7 @@ paths:
|
||||
tags:
|
||||
- Lanes
|
||||
summary: Tracked paths matching a substring (quick-open)
|
||||
description: Reads `git ls-files`, so ignored files never appear and no skip-list is maintained. Basename hits rank first. Capped at 500 paths. Read-only, no same-origin guard.
|
||||
description: Reads `git ls-files`, so ignored files never appear and no skip-list is maintained. Basename hits rank first. Capped at 500 paths. In a folder that is not a git repo it walks the disk instead (realpath-confined, no symlink followed, `.git` hidden, `node_modules` never entered, 20 000 entries at most). Read-only, no same-origin guard.
|
||||
operationId: listLanePaths
|
||||
parameters:
|
||||
- name: id
|
||||
@@ -11811,7 +11825,7 @@ paths:
|
||||
tags:
|
||||
- Lanes
|
||||
summary: One directory level of tracked files
|
||||
description: 'The file explorer''s expand-a-folder read: tracked entries under path=, folded into directories at that level. Read-only, no same-origin guard.'
|
||||
description: 'The file explorer''s expand-a-folder read: tracked entries under path=, folded into directories at that level. In a folder that is not a git repo the level is read off the disk instead (no status; `.git` hidden, `node_modules` listed as ignored and never entered). Read-only, no same-origin guard.'
|
||||
operationId: listLaneTree
|
||||
parameters:
|
||||
- name: id
|
||||
@@ -11836,7 +11850,7 @@ paths:
|
||||
tags:
|
||||
- Lanes
|
||||
summary: Every level of the tracked tree in one read
|
||||
description: 'The file explorer''s expand-all-folders read: ONE git ls-files, grouped into levels keyed by directory ("" is the root), each level shaped and capped like GET /api/lanes/{id}/git/tree''s entries. Read-only, no same-origin guard.'
|
||||
description: 'The file explorer''s expand-all-folders read: ONE git ls-files, grouped into levels keyed by directory ("" is the root), each level shaped and capped like GET /api/lanes/{id}/git/tree''s entries; a folder that is not a git repo is walked off the disk instead (at most 20 000 entries, then truncated). Read-only, no same-origin guard.'
|
||||
operationId: listLaneTreeAll
|
||||
parameters:
|
||||
- name: id
|
||||
@@ -11849,6 +11863,97 @@ paths:
|
||||
description: '{available, levels:{<dir>:[{name, path, dir}]}, truncated}.'
|
||||
'404':
|
||||
description: Lane not found.
|
||||
/api/lanes/{id}/git/download:
|
||||
get:
|
||||
tags:
|
||||
- Lanes
|
||||
summary: Download one working file of the workspace
|
||||
description: 'The Files tab''s Download: one working-copy file of any type, also in a folder that is not a git repo (server/lib/lane-file-transfer.js). Same guards as the /git/file read (lexical lane path, realpath confinement, secret refusal; under S3 a member''s file is opened through openSafe), at most 512 MB. Served as an `application/octet-stream` attachment (an ASCII filename plus `filename*=UTF-8''''…`) with `Content-Security-Policy: default-src ''none''; sandbox`, `X-Content-Type-Options: nosniff` and `Cache-Control: no-store`, so nothing in it renders or runs on the dashboard''s origin. Honours `?session=`/`?source=` like the other /git reads. Read-only, no same-origin guard.'
|
||||
operationId: downloadLaneFile
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: path
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: session
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: source
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: The file's bytes, as an attachment.
|
||||
'400':
|
||||
description: EBADPATH, ENOFILE or ETOOBIG.
|
||||
'403':
|
||||
description: 'ESECRET, or EFOREIGN (S3: the workspace folder resolves into another account''s files).'
|
||||
'404':
|
||||
description: Lane not found.
|
||||
/api/lanes/{id}/git/upload:
|
||||
post:
|
||||
tags:
|
||||
- Lanes
|
||||
summary: Upload one new file into a folder of the workspace
|
||||
description: 'The Files tab''s Upload. The body is the file''s raw bytes, its name the URI-encoded `X-File-Name` header, `dir` the workspace-relative folder ("" = the root). It only CREATES: a taken name (a symlink at it included) is 409 EEXISTS, never an overwrite. Writes into an existing folder only, never into or under `.git`, never a secret name (`settings.local.json`, a member''s home dot folder), at most 25 MB. Under S3 the file is written as the folder''s member through `write-as`; elsewhere with an exclusive create. Works in a folder that is not a git repo. Same-origin guarded.'
|
||||
operationId: uploadLaneFile
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: dir
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
default: ''
|
||||
- name: X-File-Name
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: 'The new file''s name, URI-encoded: one plain segment of at most 255 bytes.'
|
||||
- name: session
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: source
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
'201':
|
||||
description: '{path} — the workspace-relative path written.'
|
||||
'400':
|
||||
description: EBADPATH (.git), EBADNAME, ENOFILE (no such folder) or EBADCONTENT.
|
||||
'403':
|
||||
description: ESECRET, EFOREIGN, or EBADORIGIN (cross-origin).
|
||||
'404':
|
||||
description: Lane not found.
|
||||
'409':
|
||||
description: EEXISTS — a file of that name is already there.
|
||||
'413':
|
||||
description: ETOOBIG — over 25 MB.
|
||||
/api/lanes/{id}/git/branches:
|
||||
get:
|
||||
tags:
|
||||
|
||||
Reference in New Issue
Block a user