Skip to content

Commit e7c42a0

Browse files
Add live Workspace Hub diagnostics and search
1 parent 6758d5d commit e7c42a0

17 files changed

Lines changed: 1423 additions & 15 deletions

File tree

docs/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 2026-03-26
4+
5+
- Added live Workspace Hub event streaming, lightweight indexed search, and local structured failure reports inspired by reviewed upstream reference patterns.
6+
- Applied the reviewed reference patterns directly in `Workspace Hub` rather than depending on the local `tools/ref/` snapshots.
7+
38
## 2026-03-23
49

510
- Refined the skills guidance so `.agents/skills/` is documented as the native repo-level Codex location, with `shared/skills/` as shared source material and `.workspace/skills/` as an optional secondary compatibility layer.

repos/workspace-hub/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ Workspace Hub is a local control plane for people who manage many standalone rep
3737
- starts, stops, and restarts supported repos from one UI
3838
- auto-starts supported direct local repos when `Open preview` is used and the local preview is not up yet
3939
- shows runtime, install, Git, and dependency-readiness status
40+
- streams live runtime, install, cover, and activity updates from the local API
41+
- indexes repo metadata, manifests, recent logs, failure reports, and local agent-job artifacts for server-side search
4042
- stores lightweight per-repo metadata and recent activity locally
43+
- writes structured local failure reports for install and runtime errors
4144
- includes persisted appearance controls with five built-in presets and light or dark mode
4245
- reads and writes `.workspace/project.json` manifests when a repo needs explicit behaviour
4346
- captures repo cover screenshots from live previews and can insert them into repo `README.md` files
@@ -146,6 +149,8 @@ Default local endpoints:
146149

147150
- app: `http://127.0.0.1:4100`
148151
- api: `http://127.0.0.1:4101/api/health`
152+
- events: `http://127.0.0.1:4101/api/events`
153+
- search: `http://127.0.0.1:4101/api/search?q=preview`
149154

150155
## Repo Covers
151156

@@ -180,6 +185,7 @@ Workspace Hub executes repo-native commands locally. It is meant for repos you t
180185

181186
- runtime and install commands are executed through the local shell
182187
- local metadata lives under `data/` and should stay untracked
188+
- local failure reports live under `data/failure-reports/` and should stay untracked
183189
- public defaults can live in `.workspace/project.json`
184190
- local-only overrides can live in `.workspace/project.local.json`
185191

@@ -214,6 +220,7 @@ Use local override files when you want to keep your own operator notes or machin
214220

215221
- richer dependency detection beyond Node and Composer is still pending
216222
- runtime state is local and in-memory, not shared across machines
223+
- indexed search is intentionally lightweight and local-first, not a hosted code intelligence layer
217224
- the tool assumes a trusted local workspace rather than untrusted repos
218225
- synced folders such as Google Drive, iCloud, or Dropbox can interfere with `.git` directories and should be avoided
219226

repos/workspace-hub/docs/data.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ The repo should keep only ignore scaffolding here. Real metadata files stay loca
99
Typical local files:
1010

1111
- `repo-metadata.json` for per-repo notes, tags, preferred mode, and command/URL overrides.
12+
- `failure-reports/*.json` for structured runtime or install failure reports with repo, Git, health, command, and log-tail context.

repos/workspace-hub/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "workspace-hub",
33
"private": true,
4-
"version": "0.1.8",
4+
"version": "0.1.9",
55
"type": "module",
66
"packageManager": "pnpm@9.4.0",
77
"scripts": {
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
2+
import path from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
5+
import type {
6+
FailureKind,
7+
RepoFailureReportSummary,
8+
RepoInstall,
9+
RepoRuntime,
10+
WorkspaceRepo,
11+
} from '../src/types/workspace.ts'
12+
13+
type FailureSnapshotRecord = {
14+
command: string | null
15+
endedAt: string | null
16+
exitCode: number | null
17+
logTail: string[]
18+
message: string | null
19+
signal: string | null
20+
startedAt: string | null
21+
status: string
22+
updatedAt: string | null
23+
}
24+
25+
type FailureReportFile = {
26+
generatedAt: string
27+
git: WorkspaceRepo['git']
28+
health: WorkspaceRepo['health']
29+
kind: FailureKind
30+
repo: {
31+
collection: string
32+
name: string
33+
packageManager: string
34+
path: string
35+
preferredMode: WorkspaceRepo['preferredMode']
36+
previewUrl: string | null
37+
relativePath: string
38+
slug: string
39+
type: WorkspaceRepo['type']
40+
}
41+
snapshot: FailureSnapshotRecord
42+
}
43+
44+
type StoredFailureReport = FailureReportFile & {
45+
filePath: string
46+
workspaceRelativePath: string
47+
}
48+
49+
const serverFile = fileURLToPath(import.meta.url)
50+
const serverDir = path.dirname(serverFile)
51+
const appRoot = path.resolve(serverDir, '..')
52+
const configuredWorkspaceRoot = process.env.WORKSPACE_HUB_WORKSPACE_ROOT?.trim()
53+
const workspaceRoot = configuredWorkspaceRoot
54+
? path.resolve(configuredWorkspaceRoot)
55+
: path.resolve(appRoot, '..', '..')
56+
const reportsRoot = path.join(appRoot, 'data', 'failure-reports')
57+
58+
function sanitizeFileSegment(value: string) {
59+
return value
60+
.trim()
61+
.toLowerCase()
62+
.replace(/[^a-z0-9]+/g, '-')
63+
.replace(/^-+|-+$/g, '')
64+
.slice(0, 80) || 'report'
65+
}
66+
67+
function buildSnapshotRecord(
68+
kind: FailureKind,
69+
snapshot: RepoInstall | RepoRuntime,
70+
): FailureSnapshotRecord {
71+
const endedAt =
72+
kind === 'install'
73+
? (snapshot as RepoInstall).finishedAt
74+
: (snapshot as RepoRuntime).stoppedAt
75+
76+
return {
77+
command: snapshot.command,
78+
endedAt,
79+
exitCode: snapshot.lastExitCode,
80+
logTail: snapshot.logTail,
81+
message: snapshot.message,
82+
signal: snapshot.lastSignal,
83+
startedAt: snapshot.startedAt,
84+
status: snapshot.status,
85+
updatedAt: snapshot.updatedAt,
86+
}
87+
}
88+
89+
function toStoredFailureReport(
90+
filePath: string,
91+
report: FailureReportFile,
92+
): StoredFailureReport {
93+
return {
94+
...report,
95+
filePath,
96+
workspaceRelativePath: path.relative(workspaceRoot, filePath),
97+
}
98+
}
99+
100+
function toFailureReportSummary(
101+
report: StoredFailureReport,
102+
): RepoFailureReportSummary {
103+
return {
104+
command: report.snapshot.command,
105+
exitCode: report.snapshot.exitCode,
106+
filePath: report.filePath,
107+
generatedAt: report.generatedAt,
108+
kind: report.kind,
109+
message: report.snapshot.message,
110+
signal: report.snapshot.signal,
111+
workspaceRelativePath: report.workspaceRelativePath,
112+
}
113+
}
114+
115+
export async function writeFailureReport(
116+
repo: WorkspaceRepo,
117+
kind: FailureKind,
118+
snapshot: RepoInstall | RepoRuntime,
119+
) {
120+
const generatedAt = new Date().toISOString()
121+
const report: FailureReportFile = {
122+
generatedAt,
123+
git: repo.git,
124+
health: repo.health,
125+
kind,
126+
repo: {
127+
collection: repo.collection,
128+
name: repo.name,
129+
packageManager: repo.packageManager,
130+
path: repo.path,
131+
preferredMode: repo.preferredMode,
132+
previewUrl: repo.previewUrl,
133+
relativePath: repo.relativePath,
134+
slug: repo.slug,
135+
type: repo.type,
136+
},
137+
snapshot: buildSnapshotRecord(kind, snapshot),
138+
}
139+
140+
const fileName = [
141+
generatedAt.replace(/[:.]/g, '-'),
142+
sanitizeFileSegment(repo.relativePath),
143+
kind,
144+
].join('-') + '.json'
145+
const filePath = path.join(reportsRoot, fileName)
146+
147+
await mkdir(reportsRoot, { recursive: true })
148+
await writeFile(filePath, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
149+
150+
return toStoredFailureReport(filePath, report)
151+
}
152+
153+
async function readFailureReportFile(filePath: string) {
154+
try {
155+
const report = JSON.parse(await readFile(filePath, 'utf8')) as FailureReportFile
156+
157+
if (
158+
typeof report.generatedAt !== 'string' ||
159+
typeof report.kind !== 'string' ||
160+
typeof report.repo?.relativePath !== 'string'
161+
) {
162+
return null
163+
}
164+
165+
return toStoredFailureReport(filePath, report)
166+
} catch {
167+
return null
168+
}
169+
}
170+
171+
export async function readFailureReports() {
172+
try {
173+
const entries = await readdir(reportsRoot, { withFileTypes: true })
174+
const files = entries
175+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
176+
.map((entry) => path.join(reportsRoot, entry.name))
177+
178+
const reports = await Promise.all(files.map((filePath) => readFailureReportFile(filePath)))
179+
return reports.filter((report): report is StoredFailureReport => Boolean(report))
180+
} catch {
181+
return []
182+
}
183+
}
184+
185+
export async function readLatestFailureReports() {
186+
const reports = await readFailureReports()
187+
const latest = new Map<string, RepoFailureReportSummary>()
188+
189+
for (const report of reports) {
190+
const current = latest.get(report.repo.relativePath)
191+
if (!current || Date.parse(report.generatedAt) > Date.parse(current.generatedAt)) {
192+
latest.set(report.repo.relativePath, toFailureReportSummary(report))
193+
}
194+
}
195+
196+
return latest
197+
}

0 commit comments

Comments
 (0)