Skip to content

Commit e49795a

Browse files
committed
chore: stabilize the sync tree after the vendor merges
- apps/dashboard reset to our dev state (upstream vis app.ts had leaked in through rename detection while its new route files had not). - pythinker-web: repair identifiers corrupted by the swarms prose rename. - vendor rebrand script committed (scripts/upstream-sync/rebrand.mjs) with a camelCase-aware swarm rule for future snapshots. - zod deduped; lockfile refreshed. Typecheck status: 17/17 packages green, CLI green, vis green, desktop green. Remaining red (Phase 4/5 feature re-port): pythinker-web 71 errors in our feature files vs the adopted Aug-5 web internals; dashboard 19 errors vs the adopted protocol (thinkingLevel removal, renderer map for 6 new events, parseSessionMetadata export).
1 parent 251fce5 commit e49795a

22 files changed

Lines changed: 248 additions & 832 deletions

File tree

apps/dashboard/server/src/app.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,9 @@ import { PYTHINKER_CODE_HOME } from './config';
88
import { serveWebAsset, type WebAsset } from './lib/web-asset';
99
import { blobsRoute } from './routes/blobs';
1010
import { contextRoute } from './routes/context';
11-
import { cronRoute } from './routes/cron';
12-
import { importsRoute } from './routes/imports';
13-
import { logsRoute } from './routes/logs';
1411
import { sessionDetailRoute } from './routes/session-detail';
1512
import { sessionsRoute } from './routes/sessions';
1613
import { subagentsRoute } from './routes/subagents';
17-
import { tasksRoute } from './routes/tasks';
1814
import { wireRoute } from './routes/wire';
1915

2016
/** Resolve the SPA bundle directory next to the compiled server.mjs, if it
@@ -100,10 +96,6 @@ export async function createApp(options: CreateAppOptions = {}): Promise<Hono> {
10096
api.route('/sessions', wireRoute(home));
10197
api.route('/sessions', subagentsRoute(home));
10298
api.route('/sessions', blobsRoute(home));
103-
api.route('/sessions', tasksRoute(home));
104-
api.route('/sessions', cronRoute(home));
105-
api.route('/sessions', logsRoute(home));
106-
api.route('/imports', importsRoute(home));
10799
// Mount contextRoute last because it currently uses a catch-all stub
108100
// (Phase C scope) that would otherwise shadow more specific routes
109101
// registered below it.
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{"type":"metadata","protocol_version":"2.0","created_at":1779256791085}
22
{"type":"config.update","cwd":"/tmp/work","profileName":"agent","systemPrompt":"You are Pythinker.","time":1779256791100}
33
{"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"before compaction"}],"toolCalls":[]},"time":1779256800001}
4-
{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"text","text":"assistant reply"}],"toolCalls":[]},"time":1779256800200}
5-
{"type":"context.apply_compaction","summary":"compacted summary","compactedCount":2,"tokensBefore":100,"tokensAfter":30,"time":1779256800500}
4+
{"type":"context.apply_compaction","summary":"compacted summary","compactedCount":1,"tokensBefore":100,"tokensAfter":30,"time":1779256800500}
65
{"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"after compaction"}],"toolCalls":[]},"time":1779256801000}

apps/dashboard/server/test/lib/context-projector.test.ts

Lines changed: 34 additions & 200 deletions
Large diffs are not rendered by default.

apps/dashboard/server/test/routes/context.test.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,31 +70,28 @@ describe('context route', () => {
7070
cleanup = c;
7171
const app = contextRoute(home);
7272

73-
// Default (model view): the real user prompt before compaction is KEPT, the
74-
// assistant reply is dropped, then the summary, then the post-compaction tail.
73+
// Default (model view): the pre-compaction message is dropped, leaving
74+
// [summary, after-compaction].
7575
const modelRes = await app.request('/session_fixture/context?agent=main');
7676
expect(modelRes.status).toBe(200);
7777
const modelBody = (await modelRes.json()) as {
7878
messages: { source: string; message: { content: { type: string; text?: string }[] } }[];
7979
};
8080
expect(modelBody.messages.map((m) => m.source)).toEqual([
81-
'append_message', 'compaction_summary', 'append_message',
81+
'compaction_summary', 'append_message',
8282
]);
83-
expect(modelBody.messages[0]!.message.content[0]).toMatchObject({ text: 'before compaction' });
84-
expect(modelBody.messages[2]!.message.content[0]).toMatchObject({ text: 'after compaction' });
8583

86-
// Full history: every pre-compaction message (user prompt + assistant reply)
87-
// is KEPT, then the summary marker, then the post-compaction tail.
84+
// Full history: the pre-compaction message is KEPT, then the summary marker,
85+
// then the post-compaction tail.
8886
const fullRes = await app.request('/session_fixture/context?agent=main&history=full');
8987
expect(fullRes.status).toBe(200);
9088
const fullBody = (await fullRes.json()) as {
9189
messages: { source: string; message: { content: { type: string; text?: string }[] } }[];
9290
};
9391
expect(fullBody.messages.map((m) => m.source)).toEqual([
94-
'append_message', 'append_message', 'compaction_summary', 'append_message',
92+
'append_message', 'compaction_summary', 'append_message',
9593
]);
9694
expect(fullBody.messages[0]!.message.content[0]).toMatchObject({ text: 'before compaction' });
97-
expect(fullBody.messages[1]!.message.content[0]).toMatchObject({ text: 'assistant reply' });
98-
expect(fullBody.messages[3]!.message.content[0]).toMatchObject({ text: 'after compaction' });
95+
expect(fullBody.messages[2]!.message.content[0]).toMatchObject({ text: 'after compaction' });
9996
});
10097
});

apps/dashboard/web/src/api.ts

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@ import type {
55
WireResponse,
66
ContextResponse,
77
AgentTreeResponse,
8-
BackgroundTasksResponse,
9-
TaskOutputResponse,
10-
CronTasksResponse,
11-
ImportResult,
12-
LogsResponse,
138
ApiError,
149
} from './types';
1510

@@ -133,48 +128,6 @@ export const api = {
133128
getAgentTree: (id: string) =>
134129
get<AgentTreeResponse>(`/api/sessions/${enc(id)}/agents`),
135130

136-
/** Background tasks (process / agent / question) persisted under the
137-
* session's `tasks/` directory, each with `output.log` metadata. */
138-
getTasks: (id: string) =>
139-
get<BackgroundTasksResponse>(`/api/sessions/${enc(id)}/tasks`),
140-
141-
/** A byte-window of a single task's `output.log`. */
142-
getTaskOutput: (id: string, taskId: string, offset = 0, limit?: number) =>
143-
get<TaskOutputResponse>(
144-
`/api/sessions/${enc(id)}/tasks/${enc(taskId)}/output?offset=${offset}` +
145-
(limit !== undefined ? `&limit=${limit}` : ''),
146-
),
147-
148-
/** Cron jobs persisted under the session's `cron/` directory. */
149-
getCron: (id: string) =>
150-
get<CronTasksResponse>(`/api/sessions/${enc(id)}/cron`),
151-
152-
/** Parsed diagnostic log for a session (works for local and imported). */
153-
getLogs: (id: string, which: 'session' | 'global' = 'session') =>
154-
get<LogsResponse>(`/api/sessions/${enc(id)}/logs?which=${which}`),
155-
156-
/** Import a `/export-debug-zip` bundle. Sends the raw file as the body. */
157-
importZip: async (file: File): Promise<ImportResult> => {
158-
const headers: Record<string, string> = { accept: 'application/json' };
159-
const token = authToken();
160-
if (token !== null && token.length > 0) headers['authorization'] = `Bearer ${token}`;
161-
const res = await fetch(`/api/imports?name=${enc(file.name)}`, {
162-
method: 'POST',
163-
headers,
164-
body: file,
165-
});
166-
if (!res.ok) {
167-
let err: ApiError | null = null;
168-
try {
169-
err = (await res.json()) as ApiError;
170-
} catch {
171-
/* ignore */
172-
}
173-
throw new Error(err?.error ?? `HTTP ${res.status} ${res.statusText}`);
174-
}
175-
return (await res.json()) as ImportResult;
176-
},
177-
178131
deleteSession: (id: string) => del<DeleteSessionResponse>(`/api/sessions/${enc(id)}`),
179132

180133
/** Open the session's on-disk folder in the OS file manager. Side

apps/dashboard/web/src/components/layout/AppShell.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { ReactNode } from 'react';
22
import { useQueryClient } from '@tanstack/react-query';
33
import { Link } from 'react-router';
44
import { SessionRail } from '../sessions/SessionRail';
5-
import { ZipDropOverlay } from '../shared/ZipDropOverlay';
65
import { useTheme, type ThemeChoice, type ResolvedTheme } from '../../hooks/useTheme';
76

87
interface AppShellProps {
@@ -41,13 +40,8 @@ export function AppShell({ children }: AppShellProps) {
4140
</header>
4241
<div className="flex min-h-0 flex-1">
4342
<SessionRail />
44-
{/* min-w-0 lets the main column shrink below its content's intrinsic
45-
width; without it a flex child defaults to min-width:auto and wide
46-
tab content (e.g. the Timeline's flex-wrap rows) blows the layout
47-
out horizontally instead of wrapping. */}
48-
<main className="flex min-h-0 min-w-0 flex-1 flex-col">{children}</main>
43+
<main className="flex min-h-0 flex-1 flex-col">{children}</main>
4944
</div>
50-
<ZipDropOverlay />
5145
</div>
5246
);
5347
}

apps/dashboard/web/src/components/sessions/SessionCard.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,9 @@ export function SessionCard({ session, onDelete, deleting }: SessionCardProps) {
4040
<div className="flex min-w-0 items-center gap-2">
4141
<span
4242
className="inline-block h-[7px] w-[7px] shrink-0 rounded-full"
43-
style={{ backgroundColor: session.imported ? 'var(--color-cat-subagent)' : 'var(--color-fg-3)' }}
43+
style={{ backgroundColor: 'var(--color-fg-3)' }}
4444
/>
4545
<span className="shrink-0 font-mono text-[12px] text-fg-0">{shortId}</span>
46-
{session.imported ? (
47-
<span
48-
className="shrink-0 border px-1 py-0 font-mono text-[9px] uppercase tracking-[0.08em]"
49-
style={{ borderColor: 'var(--color-cat-subagent)', color: 'var(--color-cat-subagent)' }}
50-
title={
51-
session.importMeta?.originalName
52-
? `imported from ${session.importMeta.originalName}`
53-
: 'imported debug bundle'
54-
}
55-
>
56-
imported
57-
</span>
58-
) : null}
5946
</div>
6047
<span className="shrink-0 font-mono text-[10.5px] text-fg-3 tabular">
6148
{formatRelativeTime(session.updatedAt)}
@@ -73,11 +60,6 @@ export function SessionCard({ session, onDelete, deleting }: SessionCardProps) {
7360
{subagentCount}sub
7461
</span>
7562
) : null}
76-
{session.imported && session.importMeta?.manifest?.pythinkerCodeVersion ? (
77-
<span className="tabular text-fg-3" title="pythinker-code version that produced this bundle">
78-
v{session.importMeta.manifest.pythinkerCodeVersion}
79-
</span>
80-
) : null}
8163
{session.health !== 'ok' ? (
8264
<span className="tabular text-[var(--color-sev-error)]">
8365
{session.health}

apps/dashboard/web/src/components/sessions/SessionFilter.tsx

Lines changed: 6 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { useRef } from 'react';
2-
3-
import type { SessionSortKey, HealthFilter, SourceFilter } from './SessionRail';
1+
import type { SessionSortKey, HealthFilter } from './SessionRail';
42

53
interface SessionFilterProps {
64
search: string;
@@ -9,13 +7,8 @@ interface SessionFilterProps {
97
onSortChange: (v: SessionSortKey) => void;
108
healthFilter: HealthFilter;
119
onHealthChange: (v: HealthFilter) => void;
12-
sourceFilter: SourceFilter;
13-
onSourceChange: (v: SourceFilter) => void;
1410
totalCount: number;
1511
filteredCount: number;
16-
importedCount: number;
17-
onImport: (file: File) => void;
18-
importing: boolean;
1912
}
2013

2114
const SORT_OPTIONS: { value: SessionSortKey; label: string }[] = [
@@ -33,55 +26,18 @@ const HEALTH_OPTIONS: { value: HealthFilter; label: string }[] = [
3326
{ value: 'missing_main_wire', label: 'no main wire' },
3427
];
3528

36-
const SOURCE_OPTIONS: { value: SourceFilter; label: string }[] = [
37-
{ value: 'all', label: 'all' },
38-
{ value: 'local', label: 'local' },
39-
{ value: 'imported', label: 'imported' },
40-
];
41-
4229
export function SessionFilter({
4330
search,
4431
onSearchChange,
4532
sortKey,
4633
onSortChange,
4734
healthFilter,
4835
onHealthChange,
49-
sourceFilter,
50-
onSourceChange,
5136
totalCount,
5237
filteredCount,
53-
importedCount,
54-
onImport,
55-
importing,
5638
}: SessionFilterProps) {
57-
const fileInput = useRef<HTMLInputElement>(null);
5839
return (
5940
<div className="border-b border-border bg-surface-1 px-3 py-2">
60-
<div className="mb-2 flex items-center gap-2">
61-
<input
62-
ref={fileInput}
63-
type="file"
64-
accept=".zip,application/zip"
65-
className="hidden"
66-
onChange={(e) => {
67-
const file = e.target.files?.[0];
68-
if (file) onImport(file);
69-
e.target.value = '';
70-
}}
71-
/>
72-
<button
73-
type="button"
74-
disabled={importing}
75-
onClick={() => fileInput.current?.click()}
76-
className="flex items-center gap-1.5 border border-border bg-surface-0 px-2 py-1 font-mono text-[11px] text-fg-1 hover:border-border-strong hover:text-fg-0 disabled:opacity-50"
77-
title="Import a /export-debug-zip bundle a user sent you"
78-
>
79-
{importing ? 'importing…' : '⬆ import debug zip'}
80-
</button>
81-
{importedCount > 0 ? (
82-
<span className="font-mono text-[10px] text-fg-3 tabular">{importedCount} imported</span>
83-
) : null}
84-
</div>
8541
<div className="relative">
8642
<input
8743
type="text"
@@ -106,20 +62,6 @@ export function SessionFilter({
10662
))}
10763
</select>
10864
</label>
109-
<label className="flex items-center gap-1.5 font-mono text-[10.5px] text-fg-2">
110-
<span className="text-fg-3">source</span>
111-
<select
112-
value={sourceFilter}
113-
onChange={(e) => { onSourceChange(e.target.value as SourceFilter); }}
114-
className="flex-1 border border-border bg-surface-0 px-1 py-0.5 text-fg-1 focus:border-border-strong focus:outline-none"
115-
>
116-
{SOURCE_OPTIONS.map((o) => (
117-
<option key={o.value} value={o.value}>
118-
{o.label}
119-
</option>
120-
))}
121-
</select>
122-
</label>
12365
<label className="flex items-center gap-1.5 font-mono text-[10.5px] text-fg-2">
12466
<span className="text-fg-3">health</span>
12567
<select
@@ -134,11 +76,11 @@ export function SessionFilter({
13476
))}
13577
</select>
13678
</label>
137-
<div className="flex items-center justify-end">
138-
<span className="font-mono text-[10px] text-fg-3 tabular">
139-
{filteredCount} / {totalCount}
140-
</span>
141-
</div>
79+
</div>
80+
<div className="mt-2 flex items-center justify-end">
81+
<span className="font-mono text-[10px] text-fg-3 tabular">
82+
{filteredCount} / {totalCount}
83+
</span>
14284
</div>
14385
</div>
14486
);

apps/dashboard/web/src/components/sessions/SessionRail.tsx

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { useMemo, useState } from 'react';
22
import { useNavigate, useParams } from 'react-router';
33

4-
import { useDeleteSession, useImportZip, useSessions } from '../../hooks/useSession';
4+
import { useDeleteSession, useSessions } from '../../hooks/useSession';
55
import type { SessionSummary, SessionHealth } from '../../types';
66
import { SessionCard } from './SessionCard';
77
import { SessionFilter } from './SessionFilter';
88

99
export type SessionSortKey = 'recent' | 'oldest' | 'most_records' | 'most_subagents';
1010
export type HealthFilter = 'all' | SessionHealth;
11-
export type SourceFilter = 'all' | 'local' | 'imported';
1211

1312
function workspaceKey(s: SessionSummary): string {
1413
if (!s.workDir) return '(no workspace)';
@@ -31,45 +30,29 @@ function sortSessions(sessions: readonly SessionSummary[], key: SessionSortKey):
3130
export function SessionRail() {
3231
const { data, isLoading, error } = useSessions();
3332
const deleteSession = useDeleteSession();
34-
const importZip = useImportZip();
3533
const navigate = useNavigate();
3634
const { sessionId } = useParams<{ sessionId: string }>();
3735
const [search, setSearch] = useState('');
3836
const [sortKey, setSortKey] = useState<SessionSortKey>('recent');
3937
const [healthFilter, setHealthFilter] = useState<HealthFilter>('all');
40-
const [sourceFilter, setSourceFilter] = useState<SourceFilter>('all');
4138

4239
const filtered = useMemo(() => {
4340
if (!data) return [];
4441
const q = search.trim().toLowerCase();
4542
return data.filter((s) => {
4643
if (healthFilter !== 'all' && s.health !== healthFilter) return false;
47-
if (sourceFilter === 'local' && s.imported) return false;
48-
if (sourceFilter === 'imported' && !s.imported) return false;
4944
if (!q) return true;
5045
const hay = [
5146
s.sessionId,
5247
s.title ?? '',
5348
s.lastPrompt ?? '',
5449
s.workDir ?? '',
55-
s.importMeta?.originalName ?? '',
5650
]
5751
.join(' ')
5852
.toLowerCase();
5953
return hay.includes(q);
6054
});
61-
}, [data, search, healthFilter, sourceFilter]);
62-
63-
const importedCount = useMemo(() => (data ?? []).filter((s) => s.imported).length, [data]);
64-
65-
async function handleImport(file: File) {
66-
try {
67-
const result = await importZip.mutateAsync(file);
68-
void navigate(`/sessions/${result.sessionId}`);
69-
} catch (importError) {
70-
window.alert(`Import failed: ${importError instanceof Error ? importError.message : String(importError)}`);
71-
}
72-
}
55+
}, [data, search, healthFilter]);
7356

7457
const grouped = useMemo(() => {
7558
if (sortKey !== 'recent') return null;
@@ -124,13 +107,8 @@ export function SessionRail() {
124107
onSortChange={setSortKey}
125108
healthFilter={healthFilter}
126109
onHealthChange={setHealthFilter}
127-
sourceFilter={sourceFilter}
128-
onSourceChange={setSourceFilter}
129110
totalCount={data?.length ?? 0}
130111
filteredCount={filtered.length}
131-
importedCount={importedCount}
132-
onImport={(file) => { void handleImport(file); }}
133-
importing={importZip.isPending}
134112
/>
135113
<div className="min-h-0 flex-1 overflow-y-auto">
136114
{isLoading ? (

0 commit comments

Comments
 (0)