total-recall-brain 3.28.2 → 3.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/total-recall.mjs +24 -1
- package/docs/ARCHITECTURE.md +26 -18
- package/extension/background.js +259 -84
- package/extension/content-script.css +7 -13
- package/extension/content-script.js +252 -477
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/icons/mark.svg +6 -0
- package/extension/lib/brain-client.js +160 -73
- package/extension/lib/tokens.css +187 -0
- package/extension/lib/ui-helpers.js +229 -0
- package/extension/lib/ui-helpers.spec.mjs +94 -0
- package/extension/manifest.json +20 -5
- package/extension/options/options.css +64 -218
- package/extension/options/options.html +91 -43
- package/extension/options/options.js +208 -85
- package/extension/sidepanel/sidepanel.css +268 -970
- package/extension/sidepanel/sidepanel.html +200 -166
- package/extension/sidepanel/sidepanel.js +788 -942
- package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/plugins.ts +124 -92
- package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
- package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
- package/frontend/src/pages/PluginsPage.tsx +454 -1218
- package/package.json +2 -1
- package/plugins/code-quality/README.md +25 -0
- package/plugins/code-quality/cli.mjs +25 -0
- package/plugins/code-quality/plugin.json +17 -0
- package/plugins/git-sentinel/README.md +27 -0
- package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
- package/plugins/git-sentinel/plugin.json +18 -0
- package/plugins/system-monitor/README.md +26 -0
- package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
- package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
- package/plugins/system-monitor/plugin.json +29 -0
- package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
- package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
- package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
- package/src/cli/agent.mjs +1 -1
- package/src/cli/app/index.mjs +493 -0
- package/src/cli/brain.mjs +33 -3
- package/src/cli/command.mjs +80 -41
- package/src/cli/harness.mjs +1 -1
- package/src/cli/init.mjs +3 -1
- package/src/cli/mesh.mjs +118 -12
- package/src/cli/plugin/create.mjs +144 -25
- package/src/cli/plugin/index.mjs +83 -53
- package/src/cli/plugin/install.mjs +10 -127
- package/src/cli/plugin/list.mjs +44 -44
- package/src/cli/plugin/remove.mjs +23 -30
- package/src/cli/plugin/scaffold-cmd.mjs +30 -0
- package/src/cli/plugin/search.mjs +108 -23
- package/src/cli/rebuild.mjs +37 -1
- package/src/cli/recall.mjs +17 -2
- package/src/cli/research.mjs +4 -6
- package/src/cli/share.mjs +2 -0
- package/src/core/app-deploy/apply.mjs +386 -0
- package/src/core/app-deploy/design-tokens.mjs +217 -0
- package/src/core/app-deploy/plan.mjs +249 -0
- package/src/core/app-deploy/resolve.mjs +295 -0
- package/src/core/app-deploy/source.mjs +337 -0
- package/src/core/app-deploy/standalone.mjs +248 -0
- package/src/core/app-deploy/upgrade.mjs +256 -0
- package/src/core/app-deploy/verify.mjs +213 -0
- package/src/core/bound-hosts.mjs +1 -1
- package/src/core/brain-registry.mjs +146 -0
- package/src/core/brain-state.json +1 -0
- package/src/core/daemon-loop.mjs +30 -26
- package/src/core/dream.mjs +5 -17
- package/src/core/fact-seeker.mjs +53 -368
- package/src/core/lan-discovery.mjs +3 -3
- package/src/core/mesh-access-sync.mjs +134 -0
- package/src/core/mesh-access.mjs +18 -3
- package/src/core/mesh-discover.mjs +173 -0
- package/src/core/mesh.mjs +131 -32
- package/src/core/optimizer.mjs +4 -41
- package/src/core/plugin-bundle.mjs +167 -0
- package/src/core/plugin-context.mjs +8 -13
- package/src/core/plugin-loader.mjs +272 -68
- package/src/core/plugin-peers.mjs +163 -0
- package/src/core/plugin-public.mjs +85 -0
- package/src/core/plugin-runner-child.mjs +24 -0
- package/src/core/plugin-runner.mjs +102 -0
- package/src/core/plugin-store.mjs +384 -0
- package/src/core/plugin-tasks.mjs +212 -0
- package/src/core/post-mortem.mjs +40 -9
- package/src/core/research-gate.mjs +182 -0
- package/src/core/research-queue.mjs +47 -7
- package/src/core/research-surface.mjs +211 -0
- package/src/core/research.mjs +25 -32
- package/src/core/scheduler.mjs +67 -153
- package/src/core/schema.mjs +129 -0
- package/src/core/search.mjs +12 -1
- package/src/core/secret-integration-research.mjs +12 -8
- package/src/core/secrets-store.mjs +32 -2
- package/src/core/session-watcher.mjs +6 -0
- package/src/core/surface.mjs +64 -3
- package/src/core/task-executors.mjs +8 -45
- package/src/core/vault-cache.mjs +70 -4
- package/src/server/api.mjs +21 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +21 -5
- package/src/server/routes/collab.mjs +38 -11
- package/src/server/routes/extension.mjs +3 -2
- package/src/server/routes/memory.mjs +24 -2
- package/src/server/routes/mesh.mjs +7 -7
- package/src/server/routes/plugins-mesh.mjs +49 -0
- package/src/server/routes/plugins.mjs +91 -426
- package/src/server/routes/research.mjs +5 -2
- package/src/server/routes/share.mjs +2 -0
- package/src/server/tools.mjs +29 -2
- package/extension/icons/README.md +0 -27
- package/extension/lib/preconfigured.js +0 -5
- package/extension/popup/popup.css +0 -227
- package/extension/popup/popup.html +0 -50
- package/extension/popup/popup.js +0 -175
- package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
- package/scaffold/.agent/plugins/.gitkeep +0 -0
- package/scaffold/.agent/plugins/code-quality/README.md +0 -16
- package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
- package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
- package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
- package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
- package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
- package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
- package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
- package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
- package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
package/frontend/dist/index.html
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
18
18
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
19
19
|
<title>Total Recall — Portable Memory</title>
|
|
20
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
+
<script type="module" crossorigin src="/assets/index-DGMNZi_1.js"></script>
|
|
21
21
|
<link rel="stylesheet" crossorigin href="/assets/index-C7_Xpk27.css">
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
|
@@ -1,61 +1,130 @@
|
|
|
1
1
|
// ─── Plugins domain ───────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Every field here is something the node can verify: the manifest, where the
|
|
4
|
+
// plugin came from, its content hash, whether it is shared, and — for peers —
|
|
5
|
+
// whether that peer answered. There are no ratings, reviews or install counts.
|
|
2
6
|
|
|
3
7
|
import { apiFetch, API_BASE } from "./_base"
|
|
4
8
|
|
|
9
|
+
export type PluginSourceKind = "bundled" | "public" | "peer" | "git" | "local" | "link"
|
|
10
|
+
|
|
11
|
+
export interface PluginSource {
|
|
12
|
+
kind: PluginSourceKind
|
|
13
|
+
ref: string
|
|
14
|
+
peer_hostname?: string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PluginTask {
|
|
18
|
+
intent: string
|
|
19
|
+
schedule: string
|
|
20
|
+
command: string
|
|
21
|
+
last_run: string | null
|
|
22
|
+
}
|
|
23
|
+
|
|
5
24
|
export interface PluginInfo {
|
|
6
25
|
id: string
|
|
7
26
|
name: string
|
|
8
27
|
version: string
|
|
9
28
|
description: string
|
|
29
|
+
author: string | null
|
|
30
|
+
license: string | null
|
|
31
|
+
homepage: string | null
|
|
32
|
+
use_cases: string[]
|
|
10
33
|
valid: boolean
|
|
11
34
|
errors: string[]
|
|
35
|
+
scope: "project" | "global"
|
|
36
|
+
linked: boolean
|
|
12
37
|
dir: string
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
shared: boolean
|
|
39
|
+
mesh_shared: boolean
|
|
40
|
+
share_url: string | null
|
|
41
|
+
source: PluginSource
|
|
42
|
+
installed_at: string | null
|
|
43
|
+
sha256: string | null
|
|
44
|
+
installed_sha256: string | null
|
|
45
|
+
modified_since_install: boolean
|
|
46
|
+
file_count: number | null
|
|
47
|
+
size_bytes: number | null
|
|
48
|
+
categories: Array<{ name: string; description?: string; node_type?: string }>
|
|
49
|
+
tasks: PluginTask[]
|
|
50
|
+
openwiki_hubs: Array<{ title?: string; path: string }>
|
|
51
|
+
cli: { command: string | null; subcommands: Array<{ name: string; description?: string }> } | null
|
|
52
|
+
has_generator: boolean
|
|
24
53
|
manifest?: Record<string, unknown>
|
|
25
54
|
}
|
|
26
55
|
|
|
27
|
-
export interface
|
|
56
|
+
export interface BundledPlugin {
|
|
57
|
+
id: string
|
|
58
|
+
name: string
|
|
59
|
+
version: string
|
|
60
|
+
description: string
|
|
61
|
+
use_cases: string[]
|
|
62
|
+
categories: Array<{ name: string; description?: string }>
|
|
63
|
+
tasks: Array<{ intent: string; schedule: string; command: string }>
|
|
64
|
+
cli: { command: string; subcommands: Array<{ name: string; description?: string }> } | null
|
|
65
|
+
installed: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PeerPlugin {
|
|
28
69
|
id: string
|
|
29
70
|
name: string
|
|
30
71
|
version: string
|
|
31
72
|
description: string
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
userRating?: number | null
|
|
39
|
-
verified?: boolean
|
|
40
|
-
isInstalled: boolean
|
|
73
|
+
use_cases: string[]
|
|
74
|
+
sha256: string
|
|
75
|
+
file_count: number
|
|
76
|
+
size_bytes: number
|
|
77
|
+
installed: boolean
|
|
78
|
+
same_as_installed: boolean
|
|
41
79
|
}
|
|
42
80
|
|
|
43
|
-
export
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
81
|
+
export type PeerStatus = "ok" | "offline" | "unreachable" | "not_configured" | "unsupported" | "error"
|
|
82
|
+
|
|
83
|
+
export interface PeerNode {
|
|
84
|
+
hostname: string
|
|
85
|
+
ip: string
|
|
86
|
+
online: boolean
|
|
87
|
+
os: string | null
|
|
88
|
+
status: PeerStatus
|
|
89
|
+
error?: string
|
|
90
|
+
plugins: PeerPlugin[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface PeersResponse {
|
|
94
|
+
mesh: { available: boolean; configured: boolean }
|
|
95
|
+
peers: PeerNode[]
|
|
47
96
|
}
|
|
48
97
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
98
|
+
type Result = { success: boolean; message?: string; error?: string; plugin?: PluginInfo }
|
|
99
|
+
|
|
100
|
+
async function readJson(res: Response): Promise<any> {
|
|
101
|
+
try {
|
|
102
|
+
return await res.json()
|
|
103
|
+
} catch {
|
|
104
|
+
return {}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function post(url: string, body: unknown, method = "POST"): Promise<Result & Record<string, any>> {
|
|
109
|
+
try {
|
|
110
|
+
const res = await apiFetch(url, {
|
|
111
|
+
method,
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
114
|
+
})
|
|
115
|
+
const data = await readJson(res)
|
|
116
|
+
if (!res.ok) return { success: false, error: data.error || data.message || `Request failed (${res.status})` }
|
|
117
|
+
return { success: true, ...data }
|
|
118
|
+
} catch (err: any) {
|
|
119
|
+
return { success: false, error: err?.message || "Network error" }
|
|
120
|
+
}
|
|
52
121
|
}
|
|
53
122
|
|
|
54
123
|
export async function fetchPlugins(): Promise<PluginInfo[]> {
|
|
55
124
|
try {
|
|
56
125
|
const res = await apiFetch(`${API_BASE}/api/plugins`)
|
|
57
126
|
if (!res.ok) return []
|
|
58
|
-
const data
|
|
127
|
+
const data = await readJson(res)
|
|
59
128
|
return Array.isArray(data.plugins) ? data.plugins : []
|
|
60
129
|
} catch {
|
|
61
130
|
return []
|
|
@@ -66,102 +135,65 @@ export async function fetchPlugin(id: string): Promise<PluginInfo | null> {
|
|
|
66
135
|
try {
|
|
67
136
|
const res = await apiFetch(`${API_BASE}/api/plugins/${encodeURIComponent(id)}`)
|
|
68
137
|
if (!res.ok) return null
|
|
69
|
-
const data = await res
|
|
138
|
+
const data = await readJson(res)
|
|
70
139
|
return data.plugin || null
|
|
71
140
|
} catch {
|
|
72
141
|
return null
|
|
73
142
|
}
|
|
74
143
|
}
|
|
75
144
|
|
|
76
|
-
export async function
|
|
145
|
+
export async function fetchBundledPlugins(): Promise<BundledPlugin[]> {
|
|
77
146
|
try {
|
|
78
|
-
const res = await apiFetch(`${API_BASE}/api/plugins/
|
|
147
|
+
const res = await apiFetch(`${API_BASE}/api/plugins/available`)
|
|
79
148
|
if (!res.ok) return []
|
|
80
|
-
const data
|
|
81
|
-
return Array.isArray(data.
|
|
149
|
+
const data = await readJson(res)
|
|
150
|
+
return Array.isArray(data.plugins) ? data.plugins : []
|
|
82
151
|
} catch {
|
|
83
152
|
return []
|
|
84
153
|
}
|
|
85
154
|
}
|
|
86
155
|
|
|
87
|
-
|
|
156
|
+
/** Asks every mesh peer live; `null` means this node could not ask at all. */
|
|
157
|
+
export async function fetchPeerPlugins(): Promise<PeersResponse | null> {
|
|
88
158
|
try {
|
|
89
|
-
const res = await apiFetch(`${API_BASE}/api/plugins/
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (!res.ok) {
|
|
96
|
-
return { success: false, error: data.error || data.message || "Failed to install plugin" }
|
|
97
|
-
}
|
|
98
|
-
return { success: true, message: data.message }
|
|
99
|
-
} catch (err: any) {
|
|
100
|
-
return { success: false, error: err.message || "Network error" }
|
|
159
|
+
const res = await apiFetch(`${API_BASE}/api/plugins/peers`)
|
|
160
|
+
if (!res.ok) return null
|
|
161
|
+
const data = await readJson(res)
|
|
162
|
+
return { mesh: data.mesh || { available: false, configured: false }, peers: Array.isArray(data.peers) ? data.peers : [] }
|
|
163
|
+
} catch {
|
|
164
|
+
return null
|
|
101
165
|
}
|
|
102
166
|
}
|
|
103
167
|
|
|
104
|
-
export
|
|
105
|
-
|
|
106
|
-
const res = await apiFetch(`${API_BASE}/api/plugins/${encodeURIComponent(id)}?global=${global ? "true" : "false"}`, {
|
|
107
|
-
method: "DELETE"
|
|
108
|
-
})
|
|
109
|
-
const data = await res.json()
|
|
110
|
-
if (!res.ok) {
|
|
111
|
-
return { success: false, error: data.error || data.message || "Failed to remove plugin" }
|
|
112
|
-
}
|
|
113
|
-
return { success: true, message: data.message }
|
|
114
|
-
} catch (err: any) {
|
|
115
|
-
return { success: false, error: err.message || "Network error" }
|
|
116
|
-
}
|
|
168
|
+
export function installPlugin(options: { source: string; link?: boolean; global?: boolean }): Promise<Result> {
|
|
169
|
+
return post(`${API_BASE}/api/plugins/install`, options)
|
|
117
170
|
}
|
|
118
171
|
|
|
119
|
-
export
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
})
|
|
126
|
-
const data = await res.json()
|
|
127
|
-
if (!res.ok) {
|
|
128
|
-
return { success: false, error: data.error || data.message || "Failed to submit rating" }
|
|
129
|
-
}
|
|
130
|
-
return { success: true, message: data.message }
|
|
131
|
-
} catch (err: any) {
|
|
132
|
-
return { success: false, error: err.message || "Network error" }
|
|
133
|
-
}
|
|
172
|
+
export function removePlugin(id: string, global = false): Promise<Result> {
|
|
173
|
+
return post(`${API_BASE}/api/plugins/${encodeURIComponent(id)}${global ? "?global=true" : ""}`, undefined, "DELETE")
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function setPluginShared(id: string, shared: boolean): Promise<Result> {
|
|
177
|
+
return post(`${API_BASE}/api/plugins/${encodeURIComponent(id)}/share`, { shared })
|
|
134
178
|
}
|
|
135
179
|
|
|
136
180
|
export async function runPluginCommand(
|
|
137
181
|
id: string,
|
|
138
182
|
subcommand = "",
|
|
139
183
|
args: string[] = []
|
|
140
|
-
): Promise<{ success: boolean; output?: string; error?: string }> {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
headers: { "Content-Type": "application/json" },
|
|
145
|
-
body: JSON.stringify({ subcommand, args })
|
|
146
|
-
})
|
|
147
|
-
const data = await res.json()
|
|
148
|
-
if (!res.ok) {
|
|
149
|
-
return { success: false, error: data.error || data.message || "Failed to execute plugin command" }
|
|
150
|
-
}
|
|
151
|
-
return { success: true, output: data.output || "" }
|
|
152
|
-
} catch (err: any) {
|
|
153
|
-
return { success: false, error: err.message || "Network error" }
|
|
154
|
-
}
|
|
184
|
+
): Promise<{ success: boolean; output?: string; error?: string; exitCode?: number | null; timedOut?: boolean }> {
|
|
185
|
+
const res = await post(`${API_BASE}/api/plugins/${encodeURIComponent(id)}/run`, { subcommand, args })
|
|
186
|
+
if (res.error) return { success: false, error: res.error }
|
|
187
|
+
return { success: !!res.ok, output: res.output || "", exitCode: res.exitCode, timedOut: res.timedOut }
|
|
155
188
|
}
|
|
156
189
|
|
|
157
190
|
export async function fetchPluginReadme(id: string): Promise<string> {
|
|
158
191
|
try {
|
|
159
192
|
const res = await apiFetch(`${API_BASE}/api/plugins/${encodeURIComponent(id)}/readme`)
|
|
160
193
|
if (!res.ok) return ""
|
|
161
|
-
const data = await res
|
|
194
|
+
const data = await readJson(res)
|
|
162
195
|
return data.readme || ""
|
|
163
196
|
} catch {
|
|
164
197
|
return ""
|
|
165
198
|
}
|
|
166
199
|
}
|
|
167
|
-
|
|
@@ -46,6 +46,17 @@ describe('ResearchAgendaTab', () => {
|
|
|
46
46
|
expect(screen.getByPlaceholderText('e.g. Bun vs Node.js HTTP clustering performance benchmarks')).toBeInTheDocument();
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
it('labels who wanted each research item', () => {
|
|
50
|
+
const now = new Date().toISOString();
|
|
51
|
+
const items = [
|
|
52
|
+
{ id: 'u', topic: 'Asked', status: 'pending' as const, priority: 'high' as const, notes: null, created_at: now, updated_at: now, origin: 'user' as const, requested_via: 'chat' },
|
|
53
|
+
{ id: 'a', topic: 'Auto', status: 'pending' as const, priority: 'medium' as const, notes: null, created_at: now, updated_at: now, origin: 'autonomous' as const, project: 'total-recall', rationale: 'agent guessed' },
|
|
54
|
+
];
|
|
55
|
+
render(<ResearchAgendaTab {...defaultProps} researchItems={items} />);
|
|
56
|
+
expect(screen.getByText('You asked · chat')).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByText('Background research for total-recall')).toHaveAttribute('title', 'agent guessed');
|
|
58
|
+
});
|
|
59
|
+
|
|
49
60
|
it('renders research items', () => {
|
|
50
61
|
const items = [
|
|
51
62
|
{
|
|
@@ -13,6 +13,11 @@ export interface ResearchItem {
|
|
|
13
13
|
research_phase?: string
|
|
14
14
|
created_at: string
|
|
15
15
|
updated_at: string
|
|
16
|
+
/** 'user' = a human asked; 'autonomous' = background research for a project; 'legacy' = predates provenance. */
|
|
17
|
+
origin?: 'user' | 'autonomous' | 'legacy'
|
|
18
|
+
requested_via?: string | null
|
|
19
|
+
project?: string | null
|
|
20
|
+
rationale?: string | null
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
interface ResearchAgendaTabProps {
|
|
@@ -239,6 +244,16 @@ export default function ResearchAgendaTab(props: ResearchAgendaTabProps) {
|
|
|
239
244
|
</td>
|
|
240
245
|
<td style={{ padding: '14px 16px' }}>
|
|
241
246
|
<div style={{ fontWeight: 600, color: 'var(--text-primary)' }}>{item.topic}</div>
|
|
247
|
+
{(item.origin === 'autonomous' || item.origin === 'user') && (
|
|
248
|
+
<div
|
|
249
|
+
style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 2 }}
|
|
250
|
+
title={item.origin === 'autonomous' ? item.rationale || undefined : undefined}
|
|
251
|
+
>
|
|
252
|
+
{item.origin === 'autonomous'
|
|
253
|
+
? `Background research${item.project ? ` for ${item.project}` : ''}`
|
|
254
|
+
: `You asked${item.requested_via ? ` · ${item.requested_via}` : ''}`}
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
242
257
|
{item.notes && (
|
|
243
258
|
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 4 }}>
|
|
244
259
|
{item.notes}
|
|
@@ -312,25 +327,15 @@ export default function ResearchAgendaTab(props: ResearchAgendaTabProps) {
|
|
|
312
327
|
icon: '✨',
|
|
313
328
|
desc: 'Markdown polish & premium layout restructuring'
|
|
314
329
|
},
|
|
315
|
-
{
|
|
316
|
-
title: 'Source Monitoring',
|
|
317
|
-
phase: 'monitoring',
|
|
318
|
-
icon: '📡',
|
|
319
|
-
desc: 'Ongoing newsletter, RSS & release tracking'
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
title: 'Tangent Spawning',
|
|
323
|
-
phase: 'expansion',
|
|
324
|
-
icon: '🚀',
|
|
325
|
-
desc: 'Autonomously brainstorming & enqueuing tangents'
|
|
326
|
-
}
|
|
327
330
|
].map((step, idx) => {
|
|
328
|
-
|
|
331
|
+
// Research ends after improvement: it never re-monitors or
|
|
332
|
+
// spawns tangents (RESEARCH_SYSTEM2).
|
|
333
|
+
const phases = ['acquisition', 'deliberation', 'improvement']
|
|
329
334
|
const activePhase = item.research_phase || 'acquisition'
|
|
330
335
|
const activeIndex = phases.indexOf(activePhase)
|
|
331
336
|
|
|
332
337
|
let state: 'completed' | 'active' | 'waiting' | 'failed' | 'upcoming' = 'upcoming'
|
|
333
|
-
if (idx < activeIndex) {
|
|
338
|
+
if (item.status === 'done' || idx < activeIndex) {
|
|
334
339
|
state = 'completed'
|
|
335
340
|
} else if (idx === activeIndex) {
|
|
336
341
|
if (item.status === 'in_progress') state = 'active'
|
|
@@ -1,25 +1,96 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { render, screen } from '@testing-library/react';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
3
|
import { MemoryRouter } from 'react-router-dom';
|
|
4
4
|
import PluginsPage from './PluginsPage';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const installed = [{
|
|
7
|
+
id: 'git-sentinel', name: 'Git Sentinel', version: '1.1.0', description: 'Repository state',
|
|
8
|
+
author: 'Total Recall', license: 'MIT', homepage: null, use_cases: ['software-development'],
|
|
9
|
+
valid: true, errors: [], scope: 'project', linked: false, dir: '/p/git-sentinel', shared: true,
|
|
10
|
+
source: { kind: 'peer', ref: 'peer:mac-mini/git-sentinel', peer_hostname: 'mac-mini' },
|
|
11
|
+
installed_at: '2026-09-22T12:00:00.000Z', sha256: 'a'.repeat(64), installed_sha256: 'a'.repeat(64),
|
|
12
|
+
modified_since_install: false, file_count: 3, size_bytes: 2048, categories: [], tasks: [],
|
|
13
|
+
openwiki_hubs: [], cli: { command: 'git-sentinel', subcommands: [{ name: 'audit' }] }, has_generator: false
|
|
14
|
+
}];
|
|
15
|
+
|
|
16
|
+
const bundled = [
|
|
17
|
+
{ id: 'git-sentinel', name: 'Git Sentinel', version: '1.1.0', description: 'Repository state', use_cases: ['software-development'], categories: [], tasks: [], cli: null, installed: true },
|
|
18
|
+
{ id: 'system-monitor', name: 'System Monitor', version: '1.1.0', description: 'Host resources', use_cases: ['operations'], categories: [], tasks: [{ intent: 'x', schedule: '*/15 * * * *', command: 'sample' }], cli: null, installed: false }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const peers = {
|
|
22
|
+
mesh: { available: true, configured: true },
|
|
23
|
+
peers: [
|
|
24
|
+
{ hostname: 'mac-mini', ip: '100.64.0.2', online: true, os: 'macOS', status: 'ok', plugins: [
|
|
25
|
+
{ id: 'reading-list', name: 'Reading List', version: '0.2.0', description: 'Papers to read', use_cases: ['research'], sha256: 'b'.repeat(64), file_count: 4, size_bytes: 900, installed: false, same_as_installed: false }
|
|
26
|
+
] },
|
|
27
|
+
{ hostname: 'droplet', ip: '100.64.0.1', online: true, os: 'linux', status: 'unsupported', error: 'Peer does not serve plugins (older Total Recall version)', plugins: [] }
|
|
28
|
+
]
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
|
32
|
+
|
|
33
|
+
function respond(body: unknown, status = 200) {
|
|
34
|
+
return Promise.resolve({ ok: status < 400, status, json: async () => body } as Response);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
calls.length = 0;
|
|
39
|
+
globalThis.fetch = vi.fn((url: RequestInfo | URL, init?: RequestInit) => {
|
|
40
|
+
const u = String(url);
|
|
41
|
+
calls.push({ url: u, init });
|
|
42
|
+
if (u.endsWith('/api/plugins/available')) return respond({ success: true, plugins: bundled });
|
|
43
|
+
if (u.endsWith('/api/plugins/peers')) return respond({ success: true, ...peers });
|
|
44
|
+
if (u.endsWith('/api/plugins/install')) return respond({ success: true, message: 'Installed System Monitor 1.1.0 (bundled)' });
|
|
45
|
+
if (u.includes('/readme')) return respond({ success: true, readme: '# Git Sentinel' });
|
|
46
|
+
if (u.endsWith('/api/plugins')) return respond({ success: true, count: 1, plugins: installed });
|
|
47
|
+
return respond({}, 404);
|
|
48
|
+
}) as unknown as typeof fetch;
|
|
10
49
|
});
|
|
11
|
-
|
|
50
|
+
|
|
51
|
+
function renderPage() {
|
|
52
|
+
return render(<MemoryRouter><PluginsPage /></MemoryRouter>);
|
|
53
|
+
}
|
|
12
54
|
|
|
13
55
|
describe('PluginsPage', () => {
|
|
14
|
-
it('renders plugins
|
|
15
|
-
|
|
16
|
-
// Router — without one react-router throws and nothing mounts.
|
|
17
|
-
render(
|
|
18
|
-
<MemoryRouter>
|
|
19
|
-
<PluginsPage />
|
|
20
|
-
</MemoryRouter>,
|
|
21
|
-
);
|
|
22
|
-
// getByText(/Plugin/i) matched both the heading and the install button.
|
|
56
|
+
it('renders installed plugins with provenance and no fabricated social proof', async () => {
|
|
57
|
+
const { container } = renderPage();
|
|
23
58
|
expect(screen.getByRole('heading', { name: /Plugins/i })).toBeInTheDocument();
|
|
59
|
+
expect(await screen.findByText('from mac-mini')).toBeInTheDocument();
|
|
60
|
+
expect(screen.getByText('shared')).toBeInTheDocument();
|
|
61
|
+
expect(container.textContent).not.toMatch(/★|rating|reviews?\b|installs\b|downloads?|verified/i);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('installs a bundled plugin', async () => {
|
|
65
|
+
renderPage();
|
|
66
|
+
await screen.findByText('from mac-mini');
|
|
67
|
+
fireEvent.click(screen.getByRole('tab', { name: /Bundled/ }));
|
|
68
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Install' }));
|
|
69
|
+
await waitFor(() => expect(calls.some(c => c.url.endsWith('/api/plugins/install'))).toBe(true));
|
|
70
|
+
const body = JSON.parse(String(calls.find(c => c.url.endsWith('/api/plugins/install'))!.init!.body));
|
|
71
|
+
expect(body).toEqual({ source: 'system-monitor' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('shows what each mesh peer shares and reports peers it could not use', async () => {
|
|
75
|
+
const { container } = renderPage();
|
|
76
|
+
await screen.findByText('from mac-mini');
|
|
77
|
+
fireEvent.click(screen.getByRole('tab', { name: /On the mesh/ }));
|
|
78
|
+
expect(await screen.findByText('Reading List')).toBeInTheDocument();
|
|
79
|
+
expect(screen.getByText(/no plugin sharing \(older version\)/)).toBeInTheDocument();
|
|
80
|
+
fireEvent.click(screen.getByRole('button', { name: 'Install' }));
|
|
81
|
+
await waitFor(() => {
|
|
82
|
+
const req = calls.find(c => c.url.endsWith('/api/plugins/install'));
|
|
83
|
+
expect(JSON.parse(String(req!.init!.body))).toEqual({ source: 'peer:mac-mini/reading-list' });
|
|
84
|
+
});
|
|
85
|
+
expect(container.textContent).not.toMatch(/★|rating|downloads?|verified/i);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('filters by use case', async () => {
|
|
89
|
+
renderPage();
|
|
90
|
+
await screen.findByText('from mac-mini');
|
|
91
|
+
fireEvent.click(screen.getByRole('tab', { name: /Bundled/ }));
|
|
92
|
+
fireEvent.change(screen.getByLabelText('Filter by use case'), { target: { value: 'operations' } });
|
|
93
|
+
expect(screen.getByText('System Monitor')).toBeInTheDocument();
|
|
94
|
+
expect(screen.queryByText('Repository state')).not.toBeInTheDocument();
|
|
24
95
|
});
|
|
25
96
|
});
|