web: add memory document view
This commit is contained in:
parent
a5d207821d
commit
3369dbd1eb
|
|
@ -42,7 +42,9 @@ use crate::auth::{
|
|||
new_user_code, normalize_handle, parse_cookie, resolve_request_actor, rfc3339_after,
|
||||
session_set_cookie, token_hash,
|
||||
};
|
||||
use crate::authority::{ObjectiveAuthority, SqliteWorkspaceAuthority, TicketAuthority};
|
||||
use crate::authority::{
|
||||
MemoryAuthority, ObjectiveAuthority, SqliteWorkspaceAuthority, TicketAuthority,
|
||||
};
|
||||
use crate::companion::{
|
||||
CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse,
|
||||
CompanionStatusResponse, CompanionTranscriptProjection,
|
||||
|
|
@ -485,6 +487,10 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
|||
"/api/w/{workspace_id}/tickets/backend",
|
||||
post(scoped_ticket_backend_operation),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/memory",
|
||||
get(scoped_get_memory_document),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/memory/staging",
|
||||
get(scoped_list_memory_staging),
|
||||
|
|
@ -1456,6 +1462,30 @@ async fn scoped_ticket_backend_operation(
|
|||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct MemoryDocumentResponse {
|
||||
body_md: String,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
bytes: usize,
|
||||
record_source: String,
|
||||
}
|
||||
|
||||
async fn scoped_get_memory_document(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
) -> ApiResult<Json<MemoryDocumentResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
let document = api.authority.memory_document()?;
|
||||
Ok(Json(MemoryDocumentResponse {
|
||||
bytes: document.body_md.len(),
|
||||
body_md: document.body_md,
|
||||
created_at: document.created_at,
|
||||
updated_at: document.updated_at,
|
||||
record_source: document.record_source,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn scoped_list_memory_staging(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
|
|
@ -6603,8 +6633,8 @@ mod tests {
|
|||
WorkerSpawnIntent,
|
||||
};
|
||||
use crate::store::{
|
||||
MemoryStagingRecord, ObjectiveRecord, ObjectiveResourceRecord, ObjectiveTicketLinkRecord,
|
||||
SqliteWorkspaceStore,
|
||||
MemoryDocumentRecord, MemoryStagingRecord, ObjectiveRecord, ObjectiveResourceRecord,
|
||||
ObjectiveTicketLinkRecord, SqliteWorkspaceStore,
|
||||
};
|
||||
|
||||
const TEST_WORKSPACE_ID: &str = "0192f0e8-4d84-7d6e-a000-000000000001";
|
||||
|
|
@ -8423,6 +8453,15 @@ mod tests {
|
|||
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
sqlite_store
|
||||
.upsert_memory_document(&MemoryDocumentRecord {
|
||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||
body_md: "# Memory\n\n## Project facts\n\n- Frontend can read this document.\n"
|
||||
.to_string(),
|
||||
created_at: "2026-01-01T00:00:00Z".to_string(),
|
||||
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
sqlite_store
|
||||
.upsert_memory_staging_record(&MemoryStagingRecord {
|
||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||
|
|
@ -8559,6 +8598,19 @@ mod tests {
|
|||
"memory-architecture-overview.md"
|
||||
);
|
||||
|
||||
let memory_document =
|
||||
get_json(app.clone(), &format!("/api/w/{TEST_WORKSPACE_ID}/memory")).await;
|
||||
assert_eq!(memory_document["created_at"], "2026-01-01T00:00:00Z");
|
||||
assert_eq!(memory_document["updated_at"], "2026-01-02T00:00:00Z");
|
||||
assert_eq!(memory_document["bytes"], 63);
|
||||
assert_eq!(memory_document["record_source"], "workspace-sqlite");
|
||||
assert!(
|
||||
memory_document["body_md"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("Frontend can read this document.")
|
||||
);
|
||||
|
||||
let memory_staging = get_json(
|
||||
app.clone(),
|
||||
&format!("/api/w/{TEST_WORKSPACE_ID}/memory/staging?limit=10"),
|
||||
|
|
|
|||
|
|
@ -191,17 +191,26 @@ Deno.test("workspace Tickets surface uses read-only Backend Ticket APIs", async
|
|||
);
|
||||
});
|
||||
|
||||
Deno.test("workspace Memory Staging surface uses read-only scoped memory API", async () => {
|
||||
Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async () => {
|
||||
const memoryNav = await Deno.readTextFile(
|
||||
new URL("../sidebar/MemoryNavSection.svelte", import.meta.url),
|
||||
);
|
||||
const memoryLoad = await Deno.readTextFile(
|
||||
const memoryDocumentLoad = await Deno.readTextFile(
|
||||
new URL("./../../../routes/w/[workspaceId]/memory/+page.ts", import.meta.url),
|
||||
);
|
||||
const memoryDocumentPage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/memory/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const memoryStagingLoad = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/memory/staging/+page.ts",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const memoryPage = await Deno.readTextFile(
|
||||
const memoryStagingPage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/memory/staging/+page.svelte",
|
||||
import.meta.url,
|
||||
|
|
@ -209,17 +218,28 @@ Deno.test("workspace Memory Staging surface uses read-only scoped memory API", a
|
|||
);
|
||||
|
||||
assert(
|
||||
memoryNav.includes("workspaceRoute(workspaceId, '/memory')") &&
|
||||
memoryNav.includes("durable workspace memory") &&
|
||||
memoryNav.includes("workspaceRoute(workspaceId, '/memory/staging')") &&
|
||||
memoryNav.includes("pending extraction candidates"),
|
||||
"Memory sidebar section should link to the workspace Memory Staging surface",
|
||||
"Memory sidebar section should link to Document and Staging surfaces",
|
||||
);
|
||||
assert(
|
||||
memoryLoad.includes("workspaceApiPath(params.workspaceId") &&
|
||||
memoryLoad.includes('"/memory/staging"') &&
|
||||
memoryPage.includes("Memory Staging") &&
|
||||
memoryPage.includes("Workspace Server memory authority") &&
|
||||
memoryPage.includes("data.staging.data.invalid_count") &&
|
||||
memoryPage.includes("entry.record.evidence"),
|
||||
memoryDocumentLoad.includes("workspaceApiPath(params.workspaceId") &&
|
||||
memoryDocumentLoad.includes('"/memory"') &&
|
||||
memoryDocumentPage.includes("Memory Document") &&
|
||||
memoryDocumentPage.includes("This view is read-only") &&
|
||||
memoryDocumentPage.includes("data.memory.data.body_md") &&
|
||||
memoryDocumentPage.includes("data.memory.data.updated_at"),
|
||||
"Memory Document page should read the scoped API and expose the durable document without mutation controls",
|
||||
);
|
||||
assert(
|
||||
memoryStagingLoad.includes("workspaceApiPath(params.workspaceId") &&
|
||||
memoryStagingLoad.includes('"/memory/staging"') &&
|
||||
memoryStagingPage.includes("Memory Staging") &&
|
||||
memoryStagingPage.includes("Workspace Server memory authority") &&
|
||||
memoryStagingPage.includes("data.staging.data.invalid_count") &&
|
||||
memoryStagingPage.includes("entry.record.evidence"),
|
||||
"Memory Staging page should read the scoped API and expose staged records without mutation controls",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
};
|
||||
|
||||
let { currentPath = '/', workspaceId }: Props = $props();
|
||||
let documentHref = $derived(workspaceId ? workspaceRoute(workspaceId, '/memory') : '/');
|
||||
let stagingHref = $derived(workspaceId ? workspaceRoute(workspaceId, '/memory/staging') : '/');
|
||||
</script>
|
||||
|
||||
|
|
@ -15,6 +16,11 @@
|
|||
<span>Memory</span>
|
||||
</header>
|
||||
|
||||
<a class="objective-link" class:active={currentPath === documentHref} href={documentHref}>
|
||||
<span class="item-title">Document</span>
|
||||
<span class="item-meta">durable workspace memory</span>
|
||||
</a>
|
||||
|
||||
<a class="objective-link" class:active={currentPath.startsWith(stagingHref)} href={stagingHref}>
|
||||
<span class="item-title">Staging</span>
|
||||
<span class="item-meta">pending extraction candidates</span>
|
||||
|
|
|
|||
|
|
@ -318,6 +318,14 @@ export type RepositoryLogResponse = {
|
|||
diagnostics: Diagnostic[];
|
||||
};
|
||||
|
||||
export type MemoryDocumentResponse = {
|
||||
body_md: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
bytes: number;
|
||||
record_source: string;
|
||||
};
|
||||
|
||||
export type MemoryCandidateKind =
|
||||
| "preference"
|
||||
| "working_assumption"
|
||||
|
|
|
|||
123
web/workspace/src/routes/w/[workspaceId]/memory/+page.svelte
Normal file
123
web/workspace/src/routes/w/[workspaceId]/memory/+page.svelte
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<script lang="ts">
|
||||
import { formatDate } from '$lib/workspace/api/http';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
const lineCount = $derived(data.memory.data?.body_md.split('\n').length ?? 0);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Memory Document · Yoi Workspace</title>
|
||||
<meta name="description" content="Workspace Memory Document" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="card memory-document-card">
|
||||
<div class="detail-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Workspace memory</p>
|
||||
<h2>Memory Document</h2>
|
||||
</div>
|
||||
{#if data.memory.data}
|
||||
<span>{data.memory.data.bytes} bytes</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="section-note">
|
||||
Durable workspace Memory as a single Markdown document. This view is read-only.
|
||||
</p>
|
||||
|
||||
{#if data.memory.data}
|
||||
<div class="memory-document-summary" aria-label="Memory document summary">
|
||||
<div>
|
||||
<span>Updated</span>
|
||||
<strong>{formatDate(data.memory.data.updated_at)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Created</span>
|
||||
<strong>{formatDate(data.memory.data.created_at)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Lines</span>
|
||||
<strong>{lineCount}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Source</span>
|
||||
<strong>{data.memory.data.record_source}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if data.memory.data.body_md.trim().length === 0}
|
||||
<p>No Memory document content is present.</p>
|
||||
{:else}
|
||||
<pre class="memory-document-body">{data.memory.data.body_md}</pre>
|
||||
{/if}
|
||||
{:else if data.memory.error}
|
||||
<p class="error">{data.memory.error}</p>
|
||||
{:else}
|
||||
<p>Waiting for <code>/api/w/{data.workspaceId}/memory</code>…</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.memory-document-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.memory-document-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.memory-document-summary div {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.75rem;
|
||||
background: var(--bg-raised);
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.memory-document-summary span {
|
||||
color: var(--text-muted);
|
||||
display: block;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.memory-document-summary strong {
|
||||
color: var(--text);
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
margin-top: 0.25rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.memory-document-body {
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.9rem;
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.6;
|
||||
margin: 1rem 0 0;
|
||||
overflow: auto;
|
||||
padding: 1rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.memory-document-summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.memory-document-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
web/workspace/src/routes/w/[workspaceId]/memory/+page.ts
Normal file
13
web/workspace/src/routes/w/[workspaceId]/memory/+page.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import type { MemoryDocumentResponse } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
memory: await loadJson<MemoryDocumentResponse>(
|
||||
fetch,
|
||||
workspaceApiPath(params.workspaceId, "/memory"),
|
||||
),
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user