fix(frontend): session Drafts drawer uses raw_app kind for the raw-app diff (#9617)

* fix(frontend): session Drafts drawer uses raw_app kind for the raw-app diff

Follow-up to #9601. DraftDiffDrawer mapped a raw_app row back to `app` before
calling getDraftDiffValues(), but that helper sends `rawApp:true` only for the
exact kind `raw_app` (which a never-deployed raw app needs). With `app` it hit
the normal app endpoint and 404'd instead of rendering the added diff. `raw_app`
isn't in the deploy-kind maps anyway, so just pass the row kind through.

Caught by the Codex auto-review on #9601, which posted after that PR had already
merged (locked conversation), so the fix lands separately here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): show friendly draft path + summary for all kinds in session Drafts drawer

A never-deployed app/raw_app is parked at a synthetic `u/.../draft_<uuid>`
storage path with the user's typed name in the draft JSON's `draft_path`; the
Drafts drawer rendered that UUID path. The list endpoint already returns
`draft_path` and `summary` for every kind, but `fetchDrafts` dropped them and
the drawer only had the lazily-derived summary.

Thread both through the shared row: `WorkspaceDiffDrawer` gains optional
`displayPath` (shown in tree/header/search, while `path` stays the storage key
for value-loading, item keys and edit links) and `summary` (preferred over the
value-derived one, shown before the diff loads). `DraftDiffDrawer` populates
them from the draft list (`draft_path ?? path`, `summary`).

Both fields are opt-in via `?? path` / lazy fallback, so ForkDiffDrawer — the
other consumer of the component — is unchanged. The symptom only surfaced for
apps/raw apps because their storage path diverges from the friendly name;
scripts already kept a readable path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): use friendly display path for single-segment draft tree nodes

buildTree splits displayPathOf(d), but the `< 2 parts` branch still named the
file node from the storage `path` — a draft whose friendly path is a bare name
(no `/`) would show `…/draft_<uuid>` in the sidebar tree. Name it from
displayPathOf(d) too, consistent with the rest of the tree/header/search.

Addresses Codex and claude review nits on #9617.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-06-16 17:11:01 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 7cb5c6e749
commit 46288b6143
2 changed files with 41 additions and 15 deletions
@@ -61,7 +61,18 @@
const baseKind = it.raw_app ? 'raw_app' : it.kind
const kind = DEPLOY_KIND_BY_DRAFT_KIND[baseKind] ?? baseKind
donly[`${kind}/${it.path}`] = it.draft_only
return { kind, path: it.path, status: it.draft_only ? 'added' : 'modified' }
// A never-deployed app/raw_app is parked at a synthetic `…/draft_<uuid>`
// storage path with the user's typed name in `draft_path`; show that
// (matches the home list) while `path` stays the storage key for loading.
// `summary` comes straight from the draft row, so it shows for every kind
// up front instead of only after the diff value loads.
return {
kind,
path: it.path,
displayPath: it.draft_path ?? it.path,
summary: it.summary,
status: it.draft_only ? 'added' : 'modified'
}
})
draftOnlyByKey = donly
} catch (e) {
@@ -75,10 +86,11 @@
async function loadValues(d: DiffRow): Promise<{ before: unknown; after: unknown }> {
const draftOnly = draftOnlyByKey[`${d.kind}/${d.path}`] ?? false
// getDraftDiffValues works on the draft itemKind ('app' for raw apps too);
// map the deploy-style display kind back to it.
const kind: DraftKind =
d.kind === 'raw_app' ? 'app' : ((DRAFT_KIND_BY_DEPLOY_KIND[d.kind] ?? d.kind) as DraftKind)
// getDraftDiffValues keys on the draft itemKind: `raw_app` must stay
// `raw_app` (the helper sends rawApp:true only for that exact kind, which a
// never-deployed raw app needs, else it hits the normal app endpoint and
// 404s). Only the trigger display kinds map back from their deploy-style names.
const kind = (DRAFT_KIND_BY_DEPLOY_KIND[d.kind] ?? d.kind) as DraftKind
const { deployed, draft } = await getDraftDiffValues(kind, d.path, workspaceId, draftOnly)
// draft_only items have never been deployed → render as "added" (empty
// before), matching how the fork drawer renders added items.
@@ -8,6 +8,13 @@
status: DiffStatus
ahead?: number
behind?: number
/** Human-facing path; defaults to `path`. Lets a draft parked at a
* synthetic storage path (`…/draft_<uuid>`) show its friendly typed path
* while keys, value-loading and edit links stay keyed on `path`. */
displayPath?: string
/** Summary supplied by the data source. Preferred over the one derived
* from the loaded diff value, and shown before that value loads. */
summary?: string
}
</script>
@@ -88,6 +95,12 @@
return `${d.kind}/${d.path}`
}
// Friendly path for display only; `path` stays the storage key everywhere
// keys/loads happen, so a never-deployed draft still loads from `…/draft_<uuid>`.
function displayPathOf(d: DiffRow): string {
return d.displayPath ?? d.path
}
const KIND_LABELS: Record<string, string> = {
script: 'Script',
flow: 'Flow',
@@ -183,9 +196,9 @@
}
const folderCache = new Map<string, FolderNode>()
for (const d of rows) {
const parts = d.path.split('/')
const parts = displayPathOf(d).split('/')
if (parts.length < 2) {
root.children.push({ type: 'file', name: d.path, diff: d })
root.children.push({ type: 'file', name: displayPathOf(d), diff: d })
continue
}
const scopeKey = parts.slice(0, 2).join('/')
@@ -226,8 +239,8 @@
}
function searchableText(d: DiffRow): string {
const parts = [d.path, KIND_LABELS[d.kind] ?? d.kind]
const s = summaries[itemKey(d)]
const parts = [displayPathOf(d), KIND_LABELS[d.kind] ?? d.kind]
const s = summaries[itemKey(d)] ?? d.summary
if (s) parts.push(s)
return parts.join(' ')
}
@@ -433,12 +446,12 @@
<WorkspaceItemRow
kind={node.diff.kind as any}
uniformHeight
summary={summaries[key]}
summary={summaries[key] ?? node.diff.summary}
secondary={node.name}
highlighted={key === highlightedKey}
navKey={key}
indent={depth * 12 + 20}
title={node.diff.path}
title={displayPathOf(node.diff)}
onclick={() => {
highlightedKey = key
scrollToDiff(node.diff)
@@ -556,6 +569,7 @@
{@const StatusIcon = statusIcons[status]}
{@const loaded = loadedDiffs[key]}
{@const editUrl = editUrlFor?.(d)}
{@const dpath = displayPathOf(d)}
<details
open
id={rowId(d)}
@@ -573,14 +587,14 @@
{#if editUrl}
<ExternalEditLink
href={editUrl}
title={d.path}
title={dpath}
class="text-xs text-primary font-mono truncate"
>
<span class="truncate">{d.path}</span>
<span class="truncate">{dpath}</span>
</ExternalEditLink>
{:else}
<div class="text-xs text-primary font-mono truncate" title={d.path}>
{d.path}
<div class="text-xs text-primary font-mono truncate" title={dpath}>
{dpath}
</div>
{/if}
</div>