uploade
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import{stdin as I}from"process";import M from"better-sqlite3";import{join as E,dirname as y,basename as F}from"path";import{homedir as O}from"os";import{existsSync as H,mkdirSync as k}from"fs";import{fileURLToPath as x}from"url";function U(){return typeof __dirname<"u"?__dirname:y(x(import.meta.url))}var P=U(),l=process.env.CLAUDE_MEM_DATA_DIR||E(O(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||E(O(),".claude"),W=E(l,"archives"),Y=E(l,"logs"),K=E(l,"trash"),V=E(l,"backups"),q=E(l,"settings.json"),f=E(l,"claude-mem.db"),J=E(l,"vector-db"),Q=E(R,"settings.json"),z=E(R,"commands"),Z=E(R,"CLAUDE.md");function L(c){k(c,{recursive:!0})}var h=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
import{stdin as I}from"process";import M from"better-sqlite3";import{join as E,dirname as y,basename as F}from"path";import{homedir as O}from"os";import{existsSync as H,mkdirSync as k}from"fs";import{fileURLToPath as x}from"url";function U(){return typeof __dirname<"u"?__dirname:y(x(import.meta.url))}var P=U(),u=process.env.CLAUDE_MEM_DATA_DIR||E(O(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||E(O(),".claude"),W=E(u,"archives"),Y=E(u,"logs"),K=E(u,"trash"),V=E(u,"backups"),q=E(u,"settings.json"),f=E(u,"claude-mem.db"),J=E(u,"vector-db"),Q=E(R,"settings.json"),z=E(R,"commands"),Z=E(R,"CLAUDE.md");function L(c){k(c,{recursive:!0})}var h=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=h[e].padEnd(5),d=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=`
|
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=h[e].padEnd(5),d=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let l="";n!=null&&(this.level===0&&typeof n=="object"?l=`
|
||||||
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let T="";if(r){let{sessionId:m,sdkSessionId:b,correlationId:p,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([v,D])=>`${v}=${D}`).join(", ")}}`)}let S=`[${o}] [${i}] [${d}] ${_}${t}${T}${u}`;e===3?console.error(S):console.log(S)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new N;var g=class{db;constructor(){L(l),this.db=new M(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
`+JSON.stringify(n,null,2):l=" "+this.formatData(n));let T="";if(r){let{sessionId:m,sdkSessionId:b,correlationId:p,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([v,D])=>`${v}=${D}`).join(", ")}}`)}let S=`[${o}] [${i}] [${d}] ${_}${t}${T}${l}`;e===3?console.error(S):console.log(S)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new N;var g=class{db;constructor(){L(u),this.db=new M(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
version INTEGER UNIQUE NOT NULL,
|
version INTEGER UNIQUE NOT NULL,
|
||||||
@@ -317,23 +317,23 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
|||||||
INSERT INTO sdk_sessions
|
INSERT INTO sdk_sessions
|
||||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||||
VALUES (?, ?, ?, ?, ?, 'active')
|
VALUES (?, ?, ?, ?, ?, 'active')
|
||||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(`
|
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let l=this.db.prepare(`
|
||||||
INSERT INTO observations
|
INSERT INTO observations
|
||||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(`
|
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),o);return{id:Number(l.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(`
|
||||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||||
`).get(e)||(this.db.prepare(`
|
`).get(e)||(this.db.prepare(`
|
||||||
INSERT INTO sdk_sessions
|
INSERT INTO sdk_sessions
|
||||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||||
VALUES (?, ?, ?, ?, ?, 'active')
|
VALUES (?, ?, ?, ?, ?, 'active')
|
||||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(`
|
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let l=this.db.prepare(`
|
||||||
INSERT INTO session_summaries
|
INSERT INTO session_summaries
|
||||||
(sdk_session_id, project, request, investigated, learned, completed,
|
(sdk_session_id, project, request, investigated, learned, completed,
|
||||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),o);return{id:Number(l.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||||
UPDATE sdk_sessions
|
UPDATE sdk_sessions
|
||||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
@@ -384,7 +384,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
|||||||
WHERE created_at_epoch >= ? ${o}
|
WHERE created_at_epoch >= ? ${o}
|
||||||
ORDER BY created_at_epoch ASC
|
ORDER BY created_at_epoch ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`;try{let p=this.db.prepare(m).all(s,...i,t),a=this.db.prepare(b).all(s,...i,r+1);if(p.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=p.length>0?p[p.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(p){return console.error("[SessionStore] Error getting boundary timestamps:",p.message),{observations:[],sessions:[],prompts:[]}}}let u=`
|
`;try{let p=this.db.prepare(m).all(s,...i,t),a=this.db.prepare(b).all(s,...i,r+1);if(p.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=p.length>0?p[p.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(p){return console.error("[SessionStore] Error getting boundary timestamps:",p.message),{observations:[],sessions:[],prompts:[]}}}let l=`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM observations
|
FROM observations
|
||||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||||
@@ -400,5 +400,5 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
|||||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||||
ORDER BY up.created_at_epoch ASC
|
ORDER BY up.created_at_epoch ASC
|
||||||
`;try{let m=this.db.prepare(u).all(d,_,...i),b=this.db.prepare(T).all(d,_,...i),p=this.db.prepare(S).all(d,_,...i);return{observations:m,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:p.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(m){return console.error("[SessionStore] Error querying timeline records:",m.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};async function C(c){console.error("[claude-mem cleanup] Hook fired",{input:c?{session_id:c.session_id,cwd:c.cwd,reason:c.reason}:null}),c||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
`;try{let m=this.db.prepare(l).all(d,_,...i),b=this.db.prepare(T).all(d,_,...i),p=this.db.prepare(S).all(d,_,...i);return{observations:m,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:p.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(m){return console.error("[SessionStore] Error querying timeline records:",m.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};async function C(c){console.error("[claude-mem cleanup] Hook fired",{input:c?{session_id:c.session_id,cwd:c.cwd,reason:c.reason}:null}),c||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=c;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new g,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(I.isTTY)C(void 0);else{let c="";I.on("data",e=>c+=e),I.on("end",async()=>{let e=c?JSON.parse(c):void 0;await C(e)})}
|
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=c;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new g,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close();try{let n=r.worker_port||37777;await fetch(`http://127.0.0.1:${n}/sessions/${r.id}/complete`,{method:"POST",signal:AbortSignal.timeout(1e3)}),console.error("[claude-mem cleanup] Worker notified to stop processing indicator")}catch(n){console.error("[claude-mem cleanup] Failed to notify worker (non-critical):",n)}console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(I.isTTY)C(void 0);else{let c="";I.on("data",e=>c+=e),I.on("end",async()=>{let e=c?JSON.parse(c):void 0;await C(e)})}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -69,6 +69,19 @@ async function cleanupHook(input?: SessionEndInput): Promise<void> {
|
|||||||
|
|
||||||
db.close();
|
db.close();
|
||||||
|
|
||||||
|
// Tell worker to stop spinner
|
||||||
|
try {
|
||||||
|
const workerPort = session.worker_port || 37777;
|
||||||
|
await fetch(`http://127.0.0.1:${workerPort}/sessions/${session.id}/complete`, {
|
||||||
|
method: 'POST',
|
||||||
|
signal: AbortSignal.timeout(1000)
|
||||||
|
});
|
||||||
|
console.error('[claude-mem cleanup] Worker notified to stop processing indicator');
|
||||||
|
} catch (err) {
|
||||||
|
// Non-critical - worker might be down
|
||||||
|
console.error('[claude-mem cleanup] Failed to notify worker (non-critical):', err);
|
||||||
|
}
|
||||||
|
|
||||||
console.error('[claude-mem cleanup] Cleanup completed successfully');
|
console.error('[claude-mem cleanup] Cleanup completed successfully');
|
||||||
console.log('{"continue": true, "suppressOutput": true}');
|
console.log('{"continue": true, "suppressOutput": true}');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Skip routine operations:
|
|||||||
- Package installations with no errors
|
- Package installations with no errors
|
||||||
- Simple file listings
|
- Simple file listings
|
||||||
- Repetitive operations you've already documented
|
- Repetitive operations you've already documented
|
||||||
|
- If file related research comes back as empty or not found
|
||||||
- **No output necessary if skipping.**
|
- **No output necessary if skipping.**
|
||||||
|
|
||||||
OUTPUT FORMAT
|
OUTPUT FORMAT
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ class WorkerService {
|
|||||||
private sessions: Map<number, ActiveSession> = new Map();
|
private sessions: Map<number, ActiveSession> = new Map();
|
||||||
private chromaSync!: ChromaSync;
|
private chromaSync!: ChromaSync;
|
||||||
private sseClients: Set<Response> = new Set();
|
private sseClients: Set<Response> = new Set();
|
||||||
|
private isProcessing: boolean = false;
|
||||||
|
private spinnerStopTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = express();
|
this.app = express();
|
||||||
@@ -126,13 +128,14 @@ class WorkerService {
|
|||||||
this.app.get('/api/observations', this.handleGetObservations.bind(this));
|
this.app.get('/api/observations', this.handleGetObservations.bind(this));
|
||||||
this.app.get('/api/summaries', this.handleGetSummaries.bind(this));
|
this.app.get('/api/summaries', this.handleGetSummaries.bind(this));
|
||||||
this.app.get('/api/prompts', this.handleGetPrompts.bind(this));
|
this.app.get('/api/prompts', this.handleGetPrompts.bind(this));
|
||||||
|
this.app.get('/api/processing-status', this.handleGetProcessingStatus.bind(this));
|
||||||
|
|
||||||
// Session endpoints
|
// Session endpoints
|
||||||
this.app.post('/sessions/:sessionDbId/init', this.handleInit.bind(this));
|
this.app.post('/sessions/:sessionDbId/init', this.handleInit.bind(this));
|
||||||
this.app.post('/sessions/:sessionDbId/observations', this.handleObservation.bind(this));
|
this.app.post('/sessions/:sessionDbId/observations', this.handleObservation.bind(this));
|
||||||
this.app.post('/sessions/:sessionDbId/summarize', this.handleSummarize.bind(this));
|
this.app.post('/sessions/:sessionDbId/summarize', this.handleSummarize.bind(this));
|
||||||
|
this.app.post('/sessions/:sessionDbId/complete', this.handleComplete.bind(this));
|
||||||
this.app.get('/sessions/:sessionDbId/status', this.handleStatus.bind(this));
|
this.app.get('/sessions/:sessionDbId/status', this.handleStatus.bind(this));
|
||||||
this.app.delete('/sessions/:sessionDbId', this.handleDelete.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
@@ -274,16 +277,46 @@ class WorkerService {
|
|||||||
/**
|
/**
|
||||||
* Broadcast processing status to SSE clients
|
* Broadcast processing status to SSE clients
|
||||||
*/
|
*/
|
||||||
private broadcastProcessingStatus(claudeSessionId: string, isProcessing: boolean): void {
|
private broadcastProcessingStatus(isProcessing: boolean): void {
|
||||||
|
this.isProcessing = isProcessing;
|
||||||
this.broadcastSSE({
|
this.broadcastSSE({
|
||||||
type: 'processing_status',
|
type: 'processing_status',
|
||||||
processing: {
|
isProcessing
|
||||||
session_id: claudeSessionId,
|
|
||||||
is_processing: isProcessing
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if all sessions have empty queues and stop spinner after debounce
|
||||||
|
*/
|
||||||
|
private checkAndStopSpinner(): void {
|
||||||
|
// Clear any existing timer
|
||||||
|
if (this.spinnerStopTimer) {
|
||||||
|
clearTimeout(this.spinnerStopTimer);
|
||||||
|
this.spinnerStopTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any session has pending messages
|
||||||
|
const hasPendingMessages = Array.from(this.sessions.values()).some(
|
||||||
|
session => session.pendingMessages.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasPendingMessages) {
|
||||||
|
// Debounce: wait 1.5s and check again
|
||||||
|
this.spinnerStopTimer = setTimeout(() => {
|
||||||
|
const stillEmpty = Array.from(this.sessions.values()).every(
|
||||||
|
session => session.pendingMessages.length === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stillEmpty) {
|
||||||
|
logger.debug('WORKER', 'All queues empty - stopping spinner');
|
||||||
|
this.broadcastProcessingStatus(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.spinnerStopTimer = null;
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/stats - Return worker and database stats
|
* GET /api/stats - Return worker and database stats
|
||||||
*/
|
*/
|
||||||
@@ -597,6 +630,14 @@ class WorkerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/processing-status
|
||||||
|
* Returns current processing status (boolean)
|
||||||
|
*/
|
||||||
|
private handleGetProcessingStatus(_req: Request, res: Response): void {
|
||||||
|
res.json({ isProcessing: this.isProcessing });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /sessions/:sessionDbId/init
|
* POST /sessions/:sessionDbId/init
|
||||||
* Body: { project, userPrompt }
|
* Body: { project, userPrompt }
|
||||||
@@ -691,6 +732,9 @@ class WorkerService {
|
|||||||
this.sessions.delete(sessionDbId);
|
this.sessions.delete(sessionDbId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Start processing indicator (user submitted prompt)
|
||||||
|
this.broadcastProcessingStatus(true);
|
||||||
|
|
||||||
logger.success('WORKER', 'Session initialized', { sessionId: sessionDbId, port: this.port });
|
logger.success('WORKER', 'Session initialized', { sessionId: sessionDbId, port: this.port });
|
||||||
res.json({
|
res.json({
|
||||||
status: 'initialized',
|
status: 'initialized',
|
||||||
@@ -755,9 +799,6 @@ class WorkerService {
|
|||||||
prompt_number
|
prompt_number
|
||||||
});
|
});
|
||||||
|
|
||||||
// Don't broadcast processing status for observations - only for summaries
|
|
||||||
// Observations are processed continuously, skeleton should only show during summary generation
|
|
||||||
|
|
||||||
res.json({ status: 'queued', queueLength: session.pendingMessages.length });
|
res.json({ status: 'queued', queueLength: session.pendingMessages.length });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,12 +854,24 @@ class WorkerService {
|
|||||||
prompt_number
|
prompt_number
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify UI that processing is active
|
|
||||||
this.broadcastProcessingStatus(session.claudeSessionId, true);
|
|
||||||
|
|
||||||
res.json({ status: 'queued', queueLength: session.pendingMessages.length });
|
res.json({ status: 'queued', queueLength: session.pendingMessages.length });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /sessions/:sessionDbId/complete
|
||||||
|
* Called by cleanup hook to stop spinner when session ends
|
||||||
|
*/
|
||||||
|
private handleComplete(req: Request, res: Response): void {
|
||||||
|
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||||
|
|
||||||
|
logger.info('WORKER', 'Session completed - stopping spinner', { sessionId: sessionDbId });
|
||||||
|
|
||||||
|
// Stop processing indicator
|
||||||
|
this.broadcastProcessingStatus(false);
|
||||||
|
|
||||||
|
res.json({ status: 'ok' });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /sessions/:sessionDbId/status
|
* GET /sessions/:sessionDbId/status
|
||||||
*/
|
*/
|
||||||
@@ -839,42 +892,6 @@ class WorkerService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE /sessions/:sessionDbId
|
|
||||||
*/
|
|
||||||
private async handleDelete(req: Request, res: Response): Promise<void> {
|
|
||||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
|
||||||
|
|
||||||
const session = this.sessions.get(sessionDbId);
|
|
||||||
if (!session) {
|
|
||||||
res.status(404).json({ error: 'Session not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.warn('WORKER', 'Session delete requested', { sessionId: sessionDbId });
|
|
||||||
|
|
||||||
// Abort SDK agent
|
|
||||||
session.abortController.abort();
|
|
||||||
|
|
||||||
// Wait for generator to finish (with timeout)
|
|
||||||
if (session.generatorPromise) {
|
|
||||||
await Promise.race([
|
|
||||||
session.generatorPromise,
|
|
||||||
new Promise(resolve => setTimeout(resolve, 5000))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as failed since we're aborting
|
|
||||||
const db = new SessionStore();
|
|
||||||
db.markSessionFailed(sessionDbId);
|
|
||||||
db.close();
|
|
||||||
|
|
||||||
this.sessions.delete(sessionDbId);
|
|
||||||
|
|
||||||
logger.info('WORKER', 'Session deleted', { sessionId: sessionDbId });
|
|
||||||
res.json({ status: 'deleted' });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run SDK agent for a session
|
* Run SDK agent for a session
|
||||||
*/
|
*/
|
||||||
@@ -1149,9 +1166,6 @@ class WorkerService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify UI that processing is complete (summary is the final step)
|
|
||||||
this.broadcastProcessingStatus(session.claudeSessionId, false);
|
|
||||||
|
|
||||||
// Sync to Chroma (non-blocking fire-and-forget, but crash on failure)
|
// Sync to Chroma (non-blocking fire-and-forget, but crash on failure)
|
||||||
this.chromaSync.syncSummary(
|
this.chromaSync.syncSummary(
|
||||||
id,
|
id,
|
||||||
@@ -1178,12 +1192,12 @@ class WorkerService {
|
|||||||
promptNumber,
|
promptNumber,
|
||||||
contentSample: content.substring(0, 500)
|
contentSample: content.substring(0, 500)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Still mark processing as complete even if no summary was generated
|
|
||||||
this.broadcastProcessingStatus(session.claudeSessionId, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db.close();
|
db.close();
|
||||||
|
|
||||||
|
// Check if queue is empty and stop spinner after debounce
|
||||||
|
this.checkAndStopSpinner();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function App() {
|
|||||||
const [paginatedSummaries, setPaginatedSummaries] = useState<Summary[]>([]);
|
const [paginatedSummaries, setPaginatedSummaries] = useState<Summary[]>([]);
|
||||||
const [paginatedPrompts, setPaginatedPrompts] = useState<UserPrompt[]>([]);
|
const [paginatedPrompts, setPaginatedPrompts] = useState<UserPrompt[]>([]);
|
||||||
|
|
||||||
const { observations, summaries, prompts, projects, processingSessions, isConnected } = useSSE();
|
const { observations, summaries, prompts, projects, isProcessing, isConnected } = useSSE();
|
||||||
const { settings, saveSettings, isSaving, saveStatus } = useSettings();
|
const { settings, saveSettings, isSaving, saveStatus } = useSettings();
|
||||||
const { stats } = useStats();
|
const { stats } = useStats();
|
||||||
const { preference, resolvedTheme, setThemePreference } = useTheme();
|
const { preference, resolvedTheme, setThemePreference } = useTheme();
|
||||||
@@ -89,7 +89,7 @@ export function App() {
|
|||||||
onFilterChange={setCurrentFilter}
|
onFilterChange={setCurrentFilter}
|
||||||
onSettingsToggle={toggleSidebar}
|
onSettingsToggle={toggleSidebar}
|
||||||
sidebarOpen={sidebarOpen}
|
sidebarOpen={sidebarOpen}
|
||||||
isProcessing={processingSessions.size > 0}
|
isProcessing={isProcessing}
|
||||||
themePreference={preference}
|
themePreference={preference}
|
||||||
onThemeChange={setThemePreference}
|
onThemeChange={setThemePreference}
|
||||||
/>
|
/>
|
||||||
@@ -97,7 +97,6 @@ export function App() {
|
|||||||
observations={allObservations}
|
observations={allObservations}
|
||||||
summaries={allSummaries}
|
summaries={allSummaries}
|
||||||
prompts={allPrompts}
|
prompts={allPrompts}
|
||||||
processingSessions={processingSessions}
|
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
isLoading={pagination.observations.isLoading || pagination.summaries.isLoading || pagination.prompts.isLoading}
|
isLoading={pagination.observations.isLoading || pagination.summaries.isLoading || pagination.prompts.isLoading}
|
||||||
hasMore={pagination.observations.hasMore || pagination.summaries.hasMore || pagination.prompts.hasMore}
|
hasMore={pagination.observations.hasMore || pagination.summaries.hasMore || pagination.prompts.hasMore}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { useMemo, useRef, useEffect } from 'react';
|
|||||||
import { Observation, Summary, UserPrompt, FeedItem } from '../types';
|
import { Observation, Summary, UserPrompt, FeedItem } from '../types';
|
||||||
import { ObservationCard } from './ObservationCard';
|
import { ObservationCard } from './ObservationCard';
|
||||||
import { SummaryCard } from './SummaryCard';
|
import { SummaryCard } from './SummaryCard';
|
||||||
import { SummarySkeleton } from './SummarySkeleton';
|
|
||||||
import { PromptCard } from './PromptCard';
|
import { PromptCard } from './PromptCard';
|
||||||
import { UI } from '../constants/ui';
|
import { UI } from '../constants/ui';
|
||||||
|
|
||||||
@@ -10,13 +9,12 @@ interface FeedProps {
|
|||||||
observations: Observation[];
|
observations: Observation[];
|
||||||
summaries: Summary[];
|
summaries: Summary[];
|
||||||
prompts: UserPrompt[];
|
prompts: UserPrompt[];
|
||||||
processingSessions: Set<string>;
|
|
||||||
onLoadMore: () => void;
|
onLoadMore: () => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Feed({ observations, summaries, prompts, processingSessions, onLoadMore, isLoading, hasMore }: FeedProps) {
|
export function Feed({ observations, summaries, prompts, onLoadMore, isLoading, hasMore }: FeedProps) {
|
||||||
const loadMoreRef = useRef<HTMLDivElement>(null);
|
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||||
const onLoadMoreRef = useRef(onLoadMore);
|
const onLoadMoreRef = useRef(onLoadMore);
|
||||||
|
|
||||||
@@ -51,45 +49,14 @@ export function Feed({ observations, summaries, prompts, processingSessions, onL
|
|||||||
}, [hasMore, isLoading]);
|
}, [hasMore, isLoading]);
|
||||||
|
|
||||||
const items = useMemo<FeedItem[]>(() => {
|
const items = useMemo<FeedItem[]>(() => {
|
||||||
// Create a set of session IDs that already have summaries
|
|
||||||
const sessionsWithSummaries = new Set(summaries.map(s => s.session_id));
|
|
||||||
|
|
||||||
// Find the most recent prompt for each processing session
|
|
||||||
const sessionPrompts = new Map<string, UserPrompt>();
|
|
||||||
prompts.forEach(p => {
|
|
||||||
const existing = sessionPrompts.get(p.claude_session_id);
|
|
||||||
if (!existing || p.created_at_epoch > existing.created_at_epoch) {
|
|
||||||
sessionPrompts.set(p.claude_session_id, p);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create skeleton items for sessions being processed that don't have summaries yet
|
|
||||||
const skeletons: FeedItem[] = [];
|
|
||||||
processingSessions.forEach(sessionId => {
|
|
||||||
if (!sessionsWithSummaries.has(sessionId)) {
|
|
||||||
const prompt = sessionPrompts.get(sessionId);
|
|
||||||
skeletons.push({
|
|
||||||
itemType: 'skeleton',
|
|
||||||
id: sessionId, // Don't add prefix - key construction adds itemType already
|
|
||||||
session_id: sessionId,
|
|
||||||
project: prompt?.project,
|
|
||||||
// Always use current time so skeletons appear at top of feed
|
|
||||||
created_at_epoch: Date.now()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Data is already filtered by App.tsx - no need to filter again
|
|
||||||
const combined = [
|
const combined = [
|
||||||
...observations.map(o => ({ ...o, itemType: 'observation' as const })),
|
...observations.map(o => ({ ...o, itemType: 'observation' as const })),
|
||||||
...summaries.map(s => ({ ...s, itemType: 'summary' as const })),
|
...summaries.map(s => ({ ...s, itemType: 'summary' as const })),
|
||||||
...prompts.map(p => ({ ...p, itemType: 'prompt' as const })),
|
...prompts.map(p => ({ ...p, itemType: 'prompt' as const }))
|
||||||
...skeletons
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return combined
|
return combined.sort((a, b) => b.created_at_epoch - a.created_at_epoch);
|
||||||
.sort((a, b) => b.created_at_epoch - a.created_at_epoch);
|
}, [observations, summaries, prompts]);
|
||||||
}, [observations, summaries, prompts, processingSessions]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="feed">
|
<div className="feed">
|
||||||
@@ -100,8 +67,6 @@ export function Feed({ observations, summaries, prompts, processingSessions, onL
|
|||||||
return <ObservationCard key={key} observation={item} />;
|
return <ObservationCard key={key} observation={item} />;
|
||||||
} else if (item.itemType === 'summary') {
|
} else if (item.itemType === 'summary') {
|
||||||
return <SummaryCard key={key} summary={item} />;
|
return <SummaryCard key={key} summary={item} />;
|
||||||
} else if (item.itemType === 'skeleton') {
|
|
||||||
return <SummarySkeleton key={key} sessionId={item.session_id} project={item.project} />;
|
|
||||||
} else {
|
} else {
|
||||||
return <PromptCard key={key} prompt={item} />;
|
return <PromptCard key={key} prompt={item} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ export function Header({
|
|||||||
</h1>
|
</h1>
|
||||||
<div className="status">
|
<div className="status">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/thedotmack/claude-mem/"
|
href="https://docs.claude-mem.ai"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
title="GitHub"
|
title="Documentation"
|
||||||
style={{
|
style={{
|
||||||
display: 'block',
|
display: 'block',
|
||||||
padding: '8px 4px 8px 8px',
|
padding: '8px 4px 8px 8px',
|
||||||
@@ -44,7 +44,27 @@ export function Header({
|
|||||||
transition: 'color 0.2s',
|
transition: 'color 0.2s',
|
||||||
lineHeight: 0
|
lineHeight: 0
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => e.currentTarget.style.color = '#ffffff'}
|
onMouseEnter={(e) => e.currentTarget.style.color = '#606060'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.color = '#a0a0a0'}
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
|
||||||
|
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/thedotmack/claude-mem/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title="GitHub"
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
padding: '8px 4px',
|
||||||
|
color: '#a0a0a0',
|
||||||
|
transition: 'color 0.2s',
|
||||||
|
lineHeight: 0
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.color = '#606060'}
|
||||||
onMouseLeave={(e) => e.currentTarget.style.color = '#a0a0a0'}
|
onMouseLeave={(e) => e.currentTarget.style.color = '#a0a0a0'}
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
@@ -63,7 +83,7 @@ export function Header({
|
|||||||
transition: 'color 0.2s',
|
transition: 'color 0.2s',
|
||||||
lineHeight: 0
|
lineHeight: 0
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => e.currentTarget.style.color = '#ffffff'}
|
onMouseEnter={(e) => e.currentTarget.style.color = '#606060'}
|
||||||
onMouseLeave={(e) => e.currentTarget.style.color = '#a0a0a0'}
|
onMouseLeave={(e) => e.currentTarget.style.color = '#a0a0a0'}
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface SummarySkeletonProps {
|
|
||||||
sessionId: string;
|
|
||||||
project?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SummarySkeleton({ sessionId, project }: SummarySkeletonProps) {
|
|
||||||
return (
|
|
||||||
<div className="card summary-card summary-skeleton">
|
|
||||||
<div className="card-header">
|
|
||||||
<span className="card-type">SUMMARY</span>
|
|
||||||
{project && <span>{project}</span>}
|
|
||||||
<div className="processing-indicator">
|
|
||||||
<div className="spinner"></div>
|
|
||||||
<span>Generating...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="skeleton-line skeleton-title"></div>
|
|
||||||
<div className="skeleton-line skeleton-subtitle"></div>
|
|
||||||
<div className="skeleton-line skeleton-subtitle short"></div>
|
|
||||||
<div className="card-meta">Session: {sessionId}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -8,5 +8,6 @@ export const API_ENDPOINTS = {
|
|||||||
PROMPTS: '/api/prompts',
|
PROMPTS: '/api/prompts',
|
||||||
SETTINGS: '/api/settings',
|
SETTINGS: '/api/settings',
|
||||||
STATS: '/api/stats',
|
STATS: '/api/stats',
|
||||||
|
PROCESSING_STATUS: '/api/processing-status',
|
||||||
STREAM: '/stream',
|
STREAM: '/stream',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -9,10 +9,18 @@ export function useSSE() {
|
|||||||
const [prompts, setPrompts] = useState<UserPrompt[]>([]);
|
const [prompts, setPrompts] = useState<UserPrompt[]>([]);
|
||||||
const [projects, setProjects] = useState<string[]>([]);
|
const [projects, setProjects] = useState<string[]>([]);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [processingSessions, setProcessingSessions] = useState<Set<string>>(new Set());
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const eventSourceRef = useRef<EventSource | null>(null);
|
const eventSourceRef = useRef<EventSource | null>(null);
|
||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
// Fetch initial processing status on mount
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(API_ENDPOINTS.PROCESSING_STATUS)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => setIsProcessing(data.isProcessing))
|
||||||
|
.catch(err => console.error('[SSE] Failed to fetch initial processing status:', err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
// Clean up existing connection
|
// Clean up existing connection
|
||||||
@@ -70,12 +78,6 @@ export function useSSE() {
|
|||||||
const summary = data.summary;
|
const summary = data.summary;
|
||||||
console.log('[SSE] New summary:', summary.id);
|
console.log('[SSE] New summary:', summary.id);
|
||||||
setSummaries(prev => [summary, ...prev]);
|
setSummaries(prev => [summary, ...prev]);
|
||||||
// Mark session as no longer processing (summary is the final step)
|
|
||||||
setProcessingSessions(prev => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.delete(summary.session_id);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -88,18 +90,9 @@ export function useSSE() {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'processing_status':
|
case 'processing_status':
|
||||||
if (data.processing) {
|
if (typeof data.isProcessing === 'boolean') {
|
||||||
const processing = data.processing;
|
console.log('[SSE] Processing status:', data.isProcessing);
|
||||||
console.log('[SSE] Processing status:', processing);
|
setIsProcessing(data.isProcessing);
|
||||||
setProcessingSessions(prev => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (processing.is_processing) {
|
|
||||||
next.add(processing.session_id);
|
|
||||||
} else {
|
|
||||||
next.delete(processing.session_id);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -122,5 +115,5 @@ export function useSSE() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { observations, summaries, prompts, projects, processingSessions, isConnected };
|
return { observations, summaries, prompts, projects, isProcessing, isConnected };
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-13
@@ -29,18 +29,10 @@ export interface UserPrompt {
|
|||||||
created_at_epoch: number;
|
created_at_epoch: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkeletonItem {
|
|
||||||
id: string;
|
|
||||||
session_id: string;
|
|
||||||
project?: string;
|
|
||||||
created_at_epoch: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FeedItem =
|
export type FeedItem =
|
||||||
| (Observation & { itemType: 'observation' })
|
| (Observation & { itemType: 'observation' })
|
||||||
| (Summary & { itemType: 'summary' })
|
| (Summary & { itemType: 'summary' })
|
||||||
| (UserPrompt & { itemType: 'prompt' })
|
| (UserPrompt & { itemType: 'prompt' });
|
||||||
| (SkeletonItem & { itemType: 'skeleton' });
|
|
||||||
|
|
||||||
export interface StreamEvent {
|
export interface StreamEvent {
|
||||||
type: 'initial_load' | 'new_observation' | 'new_summary' | 'new_prompt' | 'processing_status';
|
type: 'initial_load' | 'new_observation' | 'new_summary' | 'new_prompt' | 'processing_status';
|
||||||
@@ -51,10 +43,7 @@ export interface StreamEvent {
|
|||||||
observation?: Observation;
|
observation?: Observation;
|
||||||
summary?: Summary;
|
summary?: Summary;
|
||||||
prompt?: UserPrompt;
|
prompt?: UserPrompt;
|
||||||
processing?: {
|
isProcessing?: boolean;
|
||||||
session_id: string;
|
|
||||||
is_processing: boolean;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
|
|||||||
Reference in New Issue
Block a user