12 KiB
Workspace Domain
This document defines the current workspace domain boundaries inside services/tuttid.
Purpose
The workspace domain owns the daemon's workspace catalog.
The current workspace record includes:
idnamelastOpenedAt
The domain supports catalog CRUD plus "open workspace" semantics through tuttid HTTP APIs and persists data in the local SQLite database.
A workspace record is not a persisted binding to one host-local directory. Local
workspace file access is resolved at runtime by workspace-scoped capabilities
and should use the resolved host absolute root instead of the legacy
/workspace alias.
Current Code Layout
services/tuttid/
api/workspace/
files.go
service.go
types.go
biz/workspace/
model.go
data/workspace/
local_files.go
store.go
sqlite_store.go
migrations.go
service/workspace/
files.go
service.go
Layer Responsibilities
api/workspace
Owns workspace transport support for the generated daemon HTTP adapter:
CatalogServiceinterface consumed byapi/DaemonAPI- mapping from domain models to generated OpenAPI response models
- mapping between generated workspace file DTOs and shared
packages/workspace/filesdomain values
Request and response DTOs are generated from services/tuttid/api/openapi/tuttid.v1.yaml.
Do not reintroduce hand-maintained workspace HTTP DTOs or a parallel workspace HTTP handler.
api/workspace should not contain SQL or local file access.
biz/workspace
Owns the smallest shared domain model that both service/ and data/ need:
Summary
Keep this layer small and domain-local. Do not move HTTP DTOs here.
service/workspace
Owns use-case orchestration:
- validate required business inputs
- generate workspace IDs for create
- update recent-open metadata when a workspace is opened
- resolve workspace-scoped capabilities without binding the catalog record to a stored host path
- compose the shared
packages/workspace/filesdomain service with daemon-owned adapters - accept transport-agnostic inputs
- return domain results for transport mapping
- call the configured store
Exception:
WorkbenchSnapshot*is a shared repository-owned contract synchronized frompackages/workbench/snapshotservice/workspacemay consume the sharedpackages/workbench/servicepackage, including the synchronized Go snapshot contract it owns, when that avoids a parallel hand-maintained mirror type- this exception does not move HTTP decoding, status-code mapping, or
route-specific validation ownership out of
api/workspace
service/workspace should not write HTTP responses, shape HTTP DTOs, or perform direct SQL.
data/workspace
Owns concrete persistence for the workspace domain.
Current responsibilities:
- define the
CatalogStoreinterface - expose
ErrWorkspaceNotFound - open the SQLite store
- run schema migration
- implement CRUD and open semantics against the
workspacestable - implement the daemon-owned local workspace file adapter that maps paths under the runtime-resolved host root; the current local-host implementation resolves that root from the user's home directory instead of a persisted workspace field
data/workspace must not depend on api/workspace.
HTTP Surface
OpenAPI is the single source of truth for exact request and response shapes, as described in API Contracts:
services/tuttid/api/openapi/tuttid.v1.yaml
This section records the workspace domain semantics behind the current generated HTTP surface, not a parallel schema.
Current workspace route semantics:
| Route | Domain meaning |
|---|---|
GET /v1/workspaces |
Lists the workspace catalog. |
POST /v1/workspaces |
Creates a workspace record. |
GET /v1/workspaces/startup |
Resolves the daemon-side startup workspace, including fallback selection and first-run default creation, so desktop does not infer restore policy from the full catalog list. |
GET /v1/workspaces/{workspaceID} |
Reads one registered workspace by id. |
POST /v1/workspaces/{workspaceID}/open |
Marks the workspace as recently opened and is used by desktop startup and dashboard launch flows. |
PATCH /v1/workspaces/{workspaceID} |
Updates mutable workspace catalog fields. |
DELETE /v1/workspaces/{workspaceID} |
Removes the workspace from the catalog. |
Workspace file route semantics:
| Route | Domain meaning |
|---|---|
GET /v1/workspaces/{workspaceID}/files/directory |
Lists direct children for one logical workspace directory. |
GET /v1/workspaces/{workspaceID}/files/search |
Searches workspace files by logical path using shared filename-first scoring. |
PUT /v1/workspaces/{workspaceID}/files/file |
Creates one empty file under the current runtime-resolved host root. |
PUT /v1/workspaces/{workspaceID}/files/directory |
Creates one directory under the current runtime-resolved host root. |
DELETE /v1/workspaces/{workspaceID}/files/entry |
Deletes one file or directory; deleting the runtime-resolved root is rejected. |
Workspace file responses use paths under the runtime-resolved host root, such as
/Users/example/project/src/main.ts, rather than the legacy /workspace/...
alias.
Workspace file routes operate on logical paths only. They must not imply that a
workspace catalog record persists a bound host directory. In the current
tuttid local-host implementation, the logical root is the user's home
directory at runtime.
Workspace File Search Ranking Rules
Workspace file search is a product-facing relevance surface, not just a raw
filesystem scan. When changing GET /v1/workspaces/{workspaceID}/files/search
or the shared scoring logic in packages/workspace/files, keep these rules:
- backend search owns primary relevance; the daemon should return the right candidates in the right order before any renderer-specific presentation work
- renderer search UI may still apply lightweight grouping or stale-result suppression, but it must not become the primary place where workspace-file relevance is fixed
- filename-first relevance is the default behavior; a query that does not express path intent should strongly prefer basename matches over deep-path fallback matches
- path-aware relevance should activate when the query expresses path intent,
especially when it includes
/; in that mode, score by path-segment order and segment-prefix quality rather than treating the entire path as one loose string - direct visible-label matches should outrank hidden-term or fallback-only matches; do not let a candidate rank highly just because the query can be stretched across an unrelated deep path
- prefix and tight-span matches should receive a strong bonus; contiguous or near-contiguous matches near the start of a basename or path segment should beat scattered subsequence matches
- deeper paths, hidden paths, and known noise directories should carry a clear penalty unless the query explicitly targets them
- daemon-owned workspace file browse and search endpoints should own hidden-item
filtering behind one shared
includeHiddenswitch; default behavior should exclude dot-prefixed files and directories, platform-hidden/system/temporary entries, Office~$lock files, and.crdownloadpartial downloads; renderer packages should not re-implement that filtering in parallel - direct file-manager reveal or navigation requests whose target directory path
already contains a dot-prefixed segment may set
includeHiddenfor that request only; do not persist this as a global "show hidden files" mode - search behavior should preserve enough metadata for the caller to explain the result, such as whether the primary hit came from basename or path matching and, when practical, the matched character positions for highlighting
Current inspiration:
- small candidate sets should follow Codex-style local fuzzy ranking: prefer visible-name hits first, then fallback terms, while rewarding prefix and tight-span matches
- file-path search should follow a dedicated path-search model rather than forcing one generic loose-string matcher to handle both basename and hierarchical path intent
Review questions for workspace-file search changes:
- Does the change improve backend relevance directly, or is it trying to hide a backend ranking problem in renderer-only code?
- For queries without
/, do obvious basename matches still beat deep-path incidental matches? - For queries with
/, does the scorer respect segment boundaries and segment order instead of rewarding arbitrary scattered path subsequences? - Can the UI still explain why the top result won, either from score shape, match target, or highlight metadata?
Error Semantics
Current rules:
- empty
namereturns400 - empty
workspaceIDreturns400 - missing workspace returns
404 - missing workspace file entries return
404 - invalid workspace file paths, root deletion, path escape attempts, and invalid
entry kinds return
400 - unexpected store failures return
502
Workspace API handlers should classify domain and adapter failures through
services/tuttid/apierrors before writing HTTP responses. For this domain:
- stable client behavior should depend on protocol
codeplus optionalreason - interpolation context should flow through structured
paramswhen needed developerMessageis diagnostic support and must not become renderer-owned UI copy- domain-specific sentinel errors still originate from
data/workspaceand shared workspace-file packages, but transport mapping belongs at the API seam
The not-found sentinel for this domain is data/workspace.ErrWorkspaceNotFound.
The shared workspace file domain owns file-specific sentinels such as
workspacefiles.ErrEntryNotFound, workspacefiles.ErrPathEscapesRoot, and
workspacefiles.ErrRootDeleteForbidden.
Local Storage
The workspace domain currently persists to the daemon-local SQLite database:
- default path:
<state-dir>/tuttid.db - state root rules are defined in Local State Storage
Current schema in data/workspace/migrations.go:
CREATE TABLE IF NOT EXISTS workspaces (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at_unix_ms INTEGER NOT NULL,
updated_at_unix_ms INTEGER NOT NULL,
last_opened_at_unix_ms INTEGER
);
Current query behavior:
- list order is
last_opened_at_unix_ms DESC, updated_at_unix_ms DESC, id ASC - create sets both
created_at_unix_msandupdated_at_unix_ms - update refreshes
updated_at_unix_ms - open refreshes both
updated_at_unix_msandlast_opened_at_unix_ms
Review Rules
When reviewing workspace changes, ask:
- Does the new type belong to
api/workspace,biz/workspace,service/workspace, ordata/workspace? - Is a transport DTO being pushed into global
types/instead of staying underapi/workspace? - Is
data/workspacedepending onapi/workspaceby accident? - Is a persistence concern leaking into
service/orapi/? - Is the new field or behavior reflected in the HTTP contract, recent-open semantics, and SQLite schema where needed?