Internal Documentation internal
TalkIDE internal documentation

Allows an authenticated user to browse the generated project directory and read the content of individual files from within the Workspace “Under-the-hood” mode. Access is restricted to the requesting user’s tenant.

  • The projects table has a dedicated slug VARCHAR(100) UNIQUE NOT NULL column. The slug is generated at project creation time using a shared utility/domain service extracted from SendMessageUseCase.extractProjectSlug() logic: if project.url is not null and contains ., take the substring before the first dot (e.g. wildwood-bakery.talkide.appwildwood-bakery); if project.url is null, use projectId.toString(). The Liquibase migration for the projects table is modified in-place (pre-production phase — DB is dropped and recreated, no backfill script needed).
  • The project directory lives on the BE server at <talkide.output-dir>/<slug>/.
  • The Files Explorer uses lazy per-folder loading. Every expand of a directory node triggers a single-level fetch of that directory’s children. The full recursive tree is never returned by a single call. The initial load of the Explorer panel fetches only the root level.
  • The set of entries visible at each level is controlled by a hierarchical whitelist configured in application.yaml (see Whitelist below). The whitelist constrains the top three levels (/, /backend, /frontend); below those levels the tree is unrestricted apart from defense-in-depth blacklist (see Blacklist below).
  • CLAUDE.md and .project-config.yml located directly in the project root are excluded from the response (they contain TalkIDE-internal context / DB credentials). Files of these names in subdirectories are returned normally.
  • BE validates every requested path against path traversal attacks: the resolved canonical path must start with the canonical project root. Any attempt to escape the root returns 404 Not Found (the response is deliberately indistinguishable from “path does not exist” so as not to leak the rule).
  • Symlinks are followed. After resolving to their real target, the canonical prefix check is applied again. Any symlink whose real target is outside the project root is excluded.
  • Binary files are detected by MIME type sniffing (Java Files.probeContentType) and by checking for null bytes in the first 8 KB. Binary files are flagged on the file content endpoint and their content is not returned as raw bytes.
  • The maximum displayable file size is 500 KB. Files larger than 500 KB are not read; BE returns a FileContentResponse with tooLarge: true, the actual size in bytes, and no content field. FE displays the error state “Soubor je příliš velký pro zobrazení (500 KB limit)”.
  • The Viewer renders file content as read-only plain text / monospace. No syntax highlighting in MVP.
  • The Explorer panel has a Refresh button in its header. Clicking it invalidates the FE per-path cache and reloads the root level. Subdirectories are then re-fetched on first expand again. No WebSocket or SSE is used in MVP.

Whitelist

TalkIDE projects have a standardized layout (backend/, docs/, frontend/). The Project Explorer shows users only the “user-facing” content, never build / deploy artefacts or BE-managed sidecar files. The whitelist is hierarchical and applies only to the top three levels:

PathAllowed directoriesAllowed files
/ (root)backend, docs, frontend(none)
/backendsrc(none)
/frontendsrcindex.html, package.json, package-lock.json, postcss.config.js, tailwind.config.js, tsconfig.json, tsconfig.node.json, vite.config.ts
/docs(all, recursive)(all, recursive)
anywhere below */src(all)(all)

Key rule: the whitelist map is keyed by parent path. A request for children of a path that has no whitelist entry returns the directory’s children unrestricted (modulo the blacklist below). This is what makes /docs, /backend/src/... and /frontend/src/... “open” subtrees while the top three levels stay tightly curated.

The whitelist is configurable via talkide.explorer.whitelist in application.yaml. To change which top-level entries appear, edit the YAML — no code change required.

Blacklist (SKIPPED_DIRS)

Defense-in-depth: a hardcoded set of directory names is always skipped, no matter where they appear in the tree. This prevents accidental leakage of build artefacts that might end up outside their standard location.

node_modules, dist, build, target, .gradle, .idea, .vscode, .git,
__pycache__, .pytest_cache, .mypy_cache, .next, .nuxt, .venv, venv

The blacklist applies even deep inside */src, where the whitelist would otherwise allow everything. It is enforced inside GetProjectFilesUseCase and is independent of the YAML whitelist.

Path Traversal Security

BE security logic applied by GetProjectFilesUseCase and GetProjectFileContentUseCase:

  1. Resolve talkide.output-dir + slugprojectRoot (canonical absolute path).
  2. Resolve projectRoot + client-supplied pathrequestedPath (canonical absolute path, following symlinks).
  3. Assert requestedPath.startsWith(projectRoot). If false → 404 Not Found.
  4. Assert requestedPath exists. If false → 404 Not Found.
  5. Apply whitelist check on the first segment of path (when it is one of the constrained levels). If the first segment is not in the allowed set for its parent, return 404 Not Found (e.g. ?path=backend/build returns 404 because build is not whitelisted under /backend).
  6. For the content endpoint only: check requestedPath is not a hidden root-level file (CLAUDE.md, .project-config.yml). If it is → 404 Not Found.

Step 2 uses Path.resolve(clientPath).normalize().toRealPath() — symlinks are resolved to their real target, and the prefix check is re-applied after resolution to prevent symlink escape.

Slug Column — Database Migration

The projects table Liquibase migration (pre-production, modified in-place) includes:

slug VARCHAR(100) NOT NULL UNIQUE

Slug validation rules (applied at project creation):

  • Lowercase letters, digits, and hyphens only
  • Kebab-case format
  • Maximum 100 characters
  • Regex: ^[a-z0-9]+(-[a-z0-9]+)*$
  • Unique constraint enforced at the DB level

API Endpoints

GET /api/v1/projects/{slug}/files — List Directory Children (lazy)

Returns the single-level children of one directory. The root level is fetched without path (or with path=); deeper levels are fetched by passing the relative path of the directory whose children should be listed.

sequenceDiagram
    actor User

    User->>+FE: opens Explorer, selects "Soubory" tab

    FE->>+BE: GET /api/v1/projects/{slug}/files <br> Authorization: Bearer {accessToken}

    BE->>BE: validate access token
    alt token missing or invalid
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
    end

    BE->>DB: load project by id
    alt project not found
        BE-->>FE: 404 Not Found <br> ErrorResponse
    end

    BE->>BE: check project.tenantId == user's tenantId
    alt project belongs to different tenant
        BE-->>FE: 403 Forbidden <br> ErrorResponse
    end

    BE->>BE: resolve project root, list root entries, apply whitelist (/) + blacklist, hide HIDDEN_ROOT_FILES

    BE->>-FE: 200 OK <br> ChildrenResponse (root entries)

    FE->>FE: cache root entries per path in Pinia store

    FE->>-User: render root level in Explorer panel

    Note over User,FE: User clicks an expand toggle on directory node

    User->>+FE: expands directory, e.g. "frontend/src"

    FE->>FE: check per-path cache
    alt children already cached
        FE-->>User: render cached children
    end

    FE->>+BE: GET /api/v1/projects/{slug}/files?path=frontend/src <br> Authorization: Bearer {accessToken}

    BE->>BE: validate access token, tenant ownership

    BE->>BE: path traversal check (canonicalize + prefix assert, symlink-safe)
    alt path escapes project root or first segment violates whitelist
        BE-->>FE: 404 Not Found <br> ErrorResponse
    end

    BE->>BE: list directory entries, apply whitelist for parent path (if any), apply SKIPPED_DIRS blacklist
    alt directory does not exist
        BE-->>FE: 404 Not Found <br> ErrorResponse
    end

    BE->>-FE: 200 OK <br> ChildrenResponse

    FE->>FE: cache children under "frontend/src"

    FE->>-User: render expanded subtree

GET /api/v1/projects/{slug}/files

Path parameters:

ParameterTypeDescription
slugstringProject slug (kebab-case, derived from project.url or project.id); see Slug Column — Database Migration

Query parameters:

ParameterRequiredDescription
pathnoURL-encoded relative path of the directory whose children should be listed. Empty (?path=) or absent → root level. Must use / as separator and not start with /.

200 OK ChildrenResponse (root level — no ?path):

{
  "path": "",
  "entries": [
    { "name": "backend", "type": "DIRECTORY" },
    { "name": "docs", "type": "DIRECTORY" },
    { "name": "frontend", "type": "DIRECTORY" }
  ]
}

200 OK ChildrenResponse (?path=frontend):

{
  "path": "frontend",
  "entries": [
    { "name": "src", "type": "DIRECTORY" },
    { "name": "index.html", "type": "FILE" },
    { "name": "package.json", "type": "FILE" },
    { "name": "package-lock.json", "type": "FILE" },
    { "name": "postcss.config.js", "type": "FILE" },
    { "name": "tailwind.config.js", "type": "FILE" },
    { "name": "tsconfig.json", "type": "FILE" },
    { "name": "tsconfig.node.json", "type": "FILE" },
    { "name": "vite.config.ts", "type": "FILE" }
  ]
}

200 OK ChildrenResponse (?path=frontend/src — open subtree below */src):

{
  "path": "frontend/src",
  "entries": [
    { "name": "components", "type": "DIRECTORY" },
    { "name": "stores", "type": "DIRECTORY" },
    { "name": "App.vue", "type": "FILE" },
    { "name": "main.ts", "type": "FILE" }
  ]
}

200 OK ChildrenResponse (project directory does not exist yet — no conversation ever started, root request):

{
  "path": "",
  "entries": []
}

401 Unauthorized (missing or invalid access token) ErrorResponse:

{
  "status": 401,
  "code": "AUTHENTICATION_FAILED",
  "message": "Access token is missing or invalid"
}

403 Forbidden (project belongs to a different tenant) ErrorResponse:

{
  "status": 403,
  "code": "FORBIDDEN",
  "message": "You do not have access to this project"
}

404 Not Found (project not found, path does not exist, path traversal attempt, or first segment not in whitelist) ErrorResponse:

{
  "status": 404,
  "code": "NOT_FOUND",
  "message": "Path not found"
}

ChildrenResponse DTO

FieldTypeNullableDescription
pathstringNOT NULLRelative path of the directory whose children are returned. Empty string for the root level. Echoes the request path parameter.
entriesEntry[]NOT NULLOrdered list of immediate children (directories first, then files; alphabetical within each group). Empty array if directory exists but is empty.

Entry DTO

FieldTypeNullableDescription
namestringNOT NULLFile or directory name (without parent path)
typestringNOT NULLDIRECTORY | FILE

Note: the lazy endpoint intentionally returns no binary flag — that information is only relevant when the user actually opens a file, and is provided by the content endpoint response.


GET /api/v1/projects/{slug}/files/content — Read File Content

sequenceDiagram
    actor User

    User->>+FE: clicks a file node in Explorer

    FE->>+BE: GET /api/v1/projects/{slug}/files/content?path=frontend/src/main.ts <br> Authorization: Bearer {accessToken}

    BE->>BE: validate access token
    alt token missing or invalid
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
    end

    BE->>DB: load project by slug column
    alt project not found
        BE-->>FE: 404 Not Found <br> ErrorResponse
    end

    BE->>BE: check tenant ownership
    alt project belongs to different tenant
        BE-->>FE: 403 Forbidden <br> ErrorResponse
    end

    BE->>BE: path traversal check (canonicalize + prefix assert, symlink-safe)
    alt path escapes project root
        BE-->>FE: 400 Bad Request <br> ErrorResponse (INVALID_PATH)
    end

    BE->>BE: check file exists
    alt file not found (or is a HIDDEN_ROOT_FILES entry)
        BE-->>FE: 404 Not Found <br> ErrorResponse
    end

    BE->>BE: check binary flag
    alt file is binary
        BE-->>FE: 400 Bad Request <br> ErrorResponse (BINARY_FILE)
    end

    BE->>BE: check file size (max 500 KB)
    alt file exceeds 500 KB
        BE-->>FE: 200 OK <br> FileContentResponse (tooLarge: true, no content)
    end

    BE->>BE: read file content (UTF-8)

    BE->>-FE: 200 OK <br> FileContentResponse

    FE->>-User: render content in Viewer (read-only, monospace)

GET /api/v1/projects/{slug}/files/content?path=frontend/src/main.ts

Query parameters:

ParameterRequiredDescription
pathyesRelative path to the file from the project root (e.g. frontend/src/main.ts, docs/README.md). Must not be empty.

200 OK FileContentResponse (file within size limit):

{
  "path": "frontend/src/main.ts",
  "name": "main.ts",
  "sizeBytes": 1234,
  "tooLarge": false,
  "content": "import { createApp } from 'vue'\nimport App from './App.vue'\n\ncreateApp(App).mount('#app')\n"
}

200 OK FileContentResponse (file exceeds 500 KB limit):

{
  "path": "frontend/src/generated-schema.ts",
  "name": "generated-schema.ts",
  "sizeBytes": 892340,
  "tooLarge": true
}

400 Bad Request (path traversal attempt) ErrorResponse:

{
  "status": 400,
  "code": "INVALID_PATH",
  "message": "Requested path is outside the project directory"
}

400 Bad Request (binary file) ErrorResponse:

{
  "status": 400,
  "code": "BINARY_FILE",
  "message": "File is binary and cannot be displayed as text"
}

401 Unauthorized (missing or invalid access token) ErrorResponse:

{
  "status": 401,
  "code": "AUTHENTICATION_FAILED",
  "message": "Access token is missing or invalid"
}

403 Forbidden (project belongs to a different tenant) ErrorResponse:

{
  "status": 403,
  "code": "FORBIDDEN",
  "message": "You do not have access to this project"
}

404 Not Found (file not found, or is a HIDDEN_ROOT_FILES entry) ErrorResponse:

{
  "status": 404,
  "code": "NOT_FOUND_FILE",
  "message": "File not found"
}

FileContentResponse DTO

FieldTypeNullableDescription
pathstringNOT NULLRelative path from project root as supplied in the request
namestringNOT NULLFile name (last segment of path)
sizeBytesintegerNOT NULLFile size in bytes
tooLargebooleanNOT NULLtrue if the file exceeds the 500 KB limit; in that case content is absent
contentstringNULLFull UTF-8 text content of the file. Absent when tooLarge is true.

Frontend

UX Guidelines

[Placeholder — Tereza doplní design spec pro Explorer panel, Viewer panel, file icons (Folder, FileText, FileCode z lucide), tree indentation, selected state, empty state, binary file badge, “too large” error state.]

  • The Explorer panel header contains a Refresh button (icon: RefreshCw from lucide). Clicking it clears the per-path children cache in the Pinia store and re-fetches the root level. Subdirectories are then re-fetched on first expand.
  • When tooLarge: true is returned by the content endpoint, FE displays the error state: “Soubor je příliš velký pro zobrazení (500 KB limit)”.

State Management — Lazy Cache

The Pinia store explorer.ts keeps a per-path cache of children:

// pseudo-code
state: {
  childrenByPath: Map<string, Entry[]>   // key = relative path; "" = root
  loadingPath: Set<string>               // paths currently being fetched
}

action loadChildren(path: string) {
  if (childrenByPath.has(path)) return                  // cache hit
  if (loadingPath.has(path)) return                     // already in flight
  loadingPath.add(path)
  const res = await api.get(`/api/v1/projects/${slug}/files`, { params: { path } })
  childrenByPath.set(path, res.entries)
  loadingPath.delete(path)
}

action refresh() {
  childrenByPath.clear()
  loadingPath.clear()
  loadChildren("")
}
  • The initial mount of the Explorer panel calls loadChildren("").
  • Each directory node renders an expand/collapse toggle. On first expand, the node calls loadChildren(node.path) and shows a per-node loading indicator until the response arrives.
  • Already-expanded nodes that are collapsed and re-expanded use the cached entries — no second fetch.

Validations

FieldConstraintsSizePatternNote
slug (path)not_blank; kebab-case1 – 100^[a-z0-9]+(-[a-z0-9]+)*$Taken from active project context; never entered manually
path (query, list endpoint)optional; if present, must not start with /, must use / separator0 – 4096URL-encoded; empty / absent means root level
path (query, content endpoint)not_blank; not start with /; use / separator1 – 4096Sent only when user clicks a non-binary file node

Backend

Configuration

PropertyDescriptionExample
talkide.output-dirAbsolute path on the BE server where all project directories live/opt/talkide/projects
talkide.explorer.max-file-size-bytesMaximum file size for the content endpoint512000 (500 KB)
talkide.explorer.whitelistHierarchical whitelist of visible top-level entries (see Whitelist)see application.yaml

Project root on disk: <talkide.output-dir>/<slug>/

Slug derivation utility (extracted from SendMessageUseCase.extractProjectSlug into a shared domain service):

  • If project.url is not null and contains .: take substring before the first dot (e.g. wildwood-bakery.talkide.appwildwood-bakery)
  • If project.url is null or has no dot: use project.id.toString()

Validations

FieldConstraintsNote
slug (path)not_blank; kebab-case (^[a-z0-9]+(-[a-z0-9]+)*$); project must exist in DB and belong to user’s tenantDirect DB lookup by slug column
path (query, list endpoint)optional; if present canonical resolved path must start with canonical project root (symlink-safe) and the first segment must satisfy the whitelist for its parent404 NOT_FOUND on any violation
path (query, content endpoint)not_blank; canonical resolved path must start with canonical project root (symlink-safe)400 INVALID_PATH if prefix check fails

Test Cases

GIVENWHENTHEN
Authenticated user, project exists and belongs to user’s tenant, project directory contains backend/, docs/, frontend/, plus CLAUDE.md and .project-config.ymlGET /api/v1/projects/{slug}/files (no path) is called200 OK; entries = backend, docs, frontend (DIRECTORY); CLAUDE.md and .project-config.yml absent; alphabetical order
Authenticated user, project root also contains an extra file notes.txt and an extra dir tmp/ (not in whitelist)GET /api/v1/projects/{slug}/files is called200 OK; notes.txt and tmp absent (root whitelist allows only backend, docs, frontend)
Authenticated user, project directory does not exist yet (first conversation never started)GET /api/v1/projects/{slug}/files is called200 OK with path="" and empty entries array
Authenticated user, ?path=frontendGET /api/v1/projects/{slug}/files is called200 OK; entries restricted to whitelist for /frontend (src directory + the listed config files); other entries hidden
Authenticated user, ?path=backendGET /api/v1/projects/{slug}/files is called200 OK; entries = src only (whitelist for /backend)
Authenticated user, ?path=docsGET /api/v1/projects/{slug}/files is called200 OK; full directory listing (no whitelist entry → unrestricted)
Authenticated user, ?path=frontend/srcGET /api/v1/projects/{slug}/files is called200 OK; full directory listing (no whitelist entry below */src → unrestricted)
Authenticated user, ?path=frontend/src/components containing node_modules/ (defense-in-depth scenario)GET /api/v1/projects/{slug}/files is called200 OK; node_modules absent (SKIPPED_DIRS blacklist)
Authenticated user, ?path=backend/build (first segment build not in /backend whitelist)GET /api/v1/projects/{slug}/files is called404 Not Found
Authenticated user, ?path=tmp (first segment not in root whitelist)GET /api/v1/projects/{slug}/files is called404 Not Found
Authenticated user, ?path=../../etc (path traversal attack)GET /api/v1/projects/{slug}/files is called404 Not Found
Authenticated user, ?path=docs/nonexistent-dirGET /api/v1/projects/{slug}/files is called404 Not Found
Authenticated user, directory contains symlink pointing outside project rootGET /api/v1/projects/{slug}/files is calledSymlink absent from entries
No Authorization headerGET /api/v1/projects/{slug}/files is called401 AUTHENTICATION_FAILED error response
Authenticated user, project belongs to a different tenantGET /api/v1/projects/{slug}/files is called403 FORBIDDEN error response
Project slug does not match any project in DBGET /api/v1/projects/{slug}/files is called404 NOT_FOUND error response
Authenticated user, project exists, user clicks file frontend/src/main.ts (within 500 KB)GET /api/v1/projects/{slug}/files/content?path=frontend/src/main.ts is called200 OK; tooLarge=false; UTF-8 content and correct sizeBytes returned
Authenticated user, file size is 600 KBGET /api/v1/projects/{slug}/files/content?path=frontend/src/large.ts is called200 OK; tooLarge=true; sizeBytes=614400; no content field
Authenticated user, path=../../etc/passwd (path traversal attack)GET /api/v1/projects/{slug}/files/content is called400 Bad Request, code INVALID_PATH
Authenticated user, path points to a symlink whose target is outside project rootGET /api/v1/projects/{slug}/files/content is called400 Bad Request, code INVALID_PATH
Authenticated user, path=CLAUDE.md (root-level filter)GET /api/v1/projects/{slug}/files/content is called404 Not Found, code NOT_FOUND_FILE
Authenticated user, path=.project-config.yml (root-level filter)GET /api/v1/projects/{slug}/files/content is called404 Not Found, code NOT_FOUND_FILE
Authenticated user, path=frontend/src/CLAUDE.md (subdirectory CLAUDE.md, not filtered)GET /api/v1/projects/{slug}/files/content is called200 OK with file content
Authenticated user, path=frontend/src/logo.png (binary file)GET /api/v1/projects/{slug}/files/content is called400 Bad Request, code BINARY_FILE
Authenticated user, path=nonexistent.txtGET /api/v1/projects/{slug}/files/content is called404 Not Found, code NOT_FOUND_FILE

Post-MVP / Future Work

  • Syntax highlighting — File content Viewer currently renders plain monospace text. Add syntax highlighting (e.g. via highlight.js or Shiki) for common languages post-MVP.
  • File encoding — Content endpoint reads UTF-8. Files encoded differently (e.g. ISO-8859-1) will appear as garbled text. Acceptable for MVP since generated project files are always UTF-8. Post-MVP: detect encoding via charset sniffing.
  • WebSocket / SSE live refresh — MVP uses a manual Refresh button. Post-MVP: push file tree change events from BE to FE (e.g. via SSE) to auto-refresh when the agent writes new files.
  • Search across the project tree — Lazy load is great for browsing but does not help when the user knows a file name and wants to jump to it. Post-MVP: server-side search endpoint that walks the (whitelisted, blacklisted) tree once and returns matching paths.

Was this page helpful?

Thanks for the feedback.