nex-level-code 0.1.0 → 0.1.2
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/LICENSE +133 -0
- package/README.md +274 -0
- package/dist/commands/sync.d.ts +10 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +235 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/webui.d.ts +9 -0
- package/dist/commands/webui.d.ts.map +1 -0
- package/dist/commands/webui.js +258 -0
- package/dist/commands/webui.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/services/install.d.ts +5 -0
- package/dist/services/install.d.ts.map +1 -1
- package/dist/services/install.js +52 -0
- package/dist/services/install.js.map +1 -1
- package/package.json +2 -2
- package/templates/hooks/memory-sync.js +216 -0
- package/templates/rules/claude.md +2 -1
- package/templates/rules/cursor.md +2 -1
- package/templates/webui/patches.diff +372 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// nlc-memory-sync.js — Memory sync hook (NLC)
|
|
3
|
+
// Syncs memory files between local Claude memory dir and a shared git repo.
|
|
4
|
+
// This enables multiple machines/instances to share the same "brain."
|
|
5
|
+
//
|
|
6
|
+
// Called as a hook:
|
|
7
|
+
// SessionStart → pull (repo → local memory)
|
|
8
|
+
// Stop/PreCompact/SessionEnd → push (local memory → repo)
|
|
9
|
+
//
|
|
10
|
+
// Config: NLC_MEMORY_REPO env var, or auto-detects from standard locations.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
// --- Config ---
|
|
17
|
+
const SYNC_FILES = ['MEMORY.md', 'session-handoff.md', 'CLAUDE.md'];
|
|
18
|
+
const DEV_LOG_PATTERN = /^\d{4}-\d{2}-\d{2}\.md$/;
|
|
19
|
+
|
|
20
|
+
// --- Find the shared memory repo ---
|
|
21
|
+
function findRepo() {
|
|
22
|
+
const home = process.env.USERPROFILE || process.env.HOME || '';
|
|
23
|
+
|
|
24
|
+
if (process.env.NLC_MEMORY_REPO && fs.existsSync(process.env.NLC_MEMORY_REPO)) {
|
|
25
|
+
return process.env.NLC_MEMORY_REPO;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const candidates = [
|
|
29
|
+
path.join(home, 'nex-memory'),
|
|
30
|
+
path.join(home, 'nlc-memory'),
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Also check common dev directories on Windows
|
|
34
|
+
if (process.platform === 'win32') {
|
|
35
|
+
const drives = ['C', 'D', 'E'];
|
|
36
|
+
for (const d of drives) {
|
|
37
|
+
candidates.push(path.join(`${d}:`, 'nex-memory'));
|
|
38
|
+
candidates.push(path.join(`${d}:`, 'dev', 'nex-memory'));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Common Linux server paths
|
|
43
|
+
if (process.platform === 'linux') {
|
|
44
|
+
candidates.push('/root/nex-memory');
|
|
45
|
+
candidates.push('/root/nlc-memory');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const p of candidates) {
|
|
49
|
+
if (fs.existsSync(path.join(p, '.git'))) return p;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Find local memory dir for current project ---
|
|
56
|
+
function cwdToProjectKey(cwd) {
|
|
57
|
+
return cwd
|
|
58
|
+
.replace(/\\/g, '-')
|
|
59
|
+
.replace(/\//g, '-')
|
|
60
|
+
.replace(/:/g, '-')
|
|
61
|
+
.replace(/_/g, '-')
|
|
62
|
+
.replace(/^([a-z])/, (m) => m.toUpperCase());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getMemoryDir(cwd) {
|
|
66
|
+
const home = process.env.USERPROFILE || process.env.HOME || '';
|
|
67
|
+
const key = cwdToProjectKey(cwd);
|
|
68
|
+
const lowerKey = key.replace(/^([A-Z])/, (m) => m.toLowerCase());
|
|
69
|
+
|
|
70
|
+
const candidates = [
|
|
71
|
+
path.join(home, '.claude', 'projects', lowerKey, 'memory'),
|
|
72
|
+
path.join(home, '.claude', 'projects', key, 'memory'),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
for (const p of candidates) {
|
|
76
|
+
if (fs.existsSync(p)) return p;
|
|
77
|
+
}
|
|
78
|
+
return candidates[0];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Git operations ---
|
|
82
|
+
function gitPull(repoPath) {
|
|
83
|
+
try {
|
|
84
|
+
execSync('git pull --rebase --quiet', { cwd: repoPath, timeout: 15000, stdio: 'pipe' });
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function gitPush(repoPath) {
|
|
92
|
+
try {
|
|
93
|
+
gitPull(repoPath);
|
|
94
|
+
|
|
95
|
+
execSync('git add -A', { cwd: repoPath, timeout: 5000, stdio: 'pipe' });
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
execSync('git diff --cached --quiet', { cwd: repoPath, timeout: 5000, stdio: 'pipe' });
|
|
99
|
+
return true; // No changes
|
|
100
|
+
} catch {
|
|
101
|
+
// Has staged changes
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
105
|
+
const hostname = require('os').hostname();
|
|
106
|
+
execSync(
|
|
107
|
+
`git commit -m "sync: ${hostname} @ ${timestamp}"`,
|
|
108
|
+
{ cwd: repoPath, timeout: 10000, stdio: 'pipe' }
|
|
109
|
+
);
|
|
110
|
+
execSync('git push --quiet', { cwd: repoPath, timeout: 15000, stdio: 'pipe' });
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- File sync ---
|
|
118
|
+
function getFilesToSync(dir) {
|
|
119
|
+
if (!fs.existsSync(dir)) return [];
|
|
120
|
+
return fs.readdirSync(dir).filter(f =>
|
|
121
|
+
SYNC_FILES.includes(f) || DEV_LOG_PATTERN.test(f)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function copyIfDifferent(src, dst) {
|
|
126
|
+
if (!fs.existsSync(src)) return false;
|
|
127
|
+
|
|
128
|
+
if (!fs.existsSync(dst)) {
|
|
129
|
+
fs.copyFileSync(src, dst);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const srcContent = fs.readFileSync(src, 'utf-8');
|
|
134
|
+
const dstContent = fs.readFileSync(dst, 'utf-8');
|
|
135
|
+
if (srcContent !== dstContent) {
|
|
136
|
+
fs.copyFileSync(src, dst);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function syncPull(repoPath, memoryDir, cwd) {
|
|
144
|
+
gitPull(repoPath);
|
|
145
|
+
|
|
146
|
+
if (!fs.existsSync(memoryDir)) fs.mkdirSync(memoryDir, { recursive: true });
|
|
147
|
+
|
|
148
|
+
const files = getFilesToSync(repoPath);
|
|
149
|
+
let copied = 0;
|
|
150
|
+
for (const f of files) {
|
|
151
|
+
if (f === 'CLAUDE.md') {
|
|
152
|
+
// CLAUDE.md goes to CWD (project root), not memory dir
|
|
153
|
+
if (copyIfDifferent(path.join(repoPath, f), path.join(cwd, f))) copied++;
|
|
154
|
+
} else {
|
|
155
|
+
if (copyIfDifferent(path.join(repoPath, f), path.join(memoryDir, f))) copied++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return copied;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function syncPush(repoPath, memoryDir, cwd) {
|
|
162
|
+
let copied = 0;
|
|
163
|
+
|
|
164
|
+
// Copy memory files from local memory dir → repo
|
|
165
|
+
if (fs.existsSync(memoryDir)) {
|
|
166
|
+
const files = getFilesToSync(memoryDir);
|
|
167
|
+
for (const f of files) {
|
|
168
|
+
if (f === 'CLAUDE.md') continue; // CLAUDE.md lives in CWD, handled below
|
|
169
|
+
if (copyIfDifferent(path.join(memoryDir, f), path.join(repoPath, f))) copied++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Copy CLAUDE.md from CWD → repo
|
|
174
|
+
const claudePath = path.join(cwd, 'CLAUDE.md');
|
|
175
|
+
if (fs.existsSync(claudePath)) {
|
|
176
|
+
if (copyIfDifferent(claudePath, path.join(repoPath, 'CLAUDE.md'))) copied++;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Also pull files from repo that only exist locally (from other machine)
|
|
180
|
+
const repoFiles = getFilesToSync(repoPath);
|
|
181
|
+
for (const f of repoFiles) {
|
|
182
|
+
if (f === 'CLAUDE.md') continue;
|
|
183
|
+
const localPath = path.join(memoryDir, f);
|
|
184
|
+
if (!fs.existsSync(localPath)) {
|
|
185
|
+
if (copyIfDifferent(path.join(repoPath, f), localPath)) copied++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
gitPush(repoPath);
|
|
190
|
+
return copied;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --- Main ---
|
|
194
|
+
let input = '';
|
|
195
|
+
process.stdin.setEncoding('utf-8');
|
|
196
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
197
|
+
process.stdin.on('end', () => {
|
|
198
|
+
try {
|
|
199
|
+
const hookData = JSON.parse(input);
|
|
200
|
+
const cwd = hookData.cwd || hookData.session_cwd || process.cwd();
|
|
201
|
+
const hookType = hookData.hook_type || hookData.type || '';
|
|
202
|
+
|
|
203
|
+
const repoPath = findRepo();
|
|
204
|
+
if (!repoPath) return; // No repo found, skip silently
|
|
205
|
+
|
|
206
|
+
const memoryDir = getMemoryDir(cwd);
|
|
207
|
+
|
|
208
|
+
if (hookType === 'SessionStart' || hookType === 'session_start') {
|
|
209
|
+
syncPull(repoPath, memoryDir, cwd);
|
|
210
|
+
} else {
|
|
211
|
+
syncPush(repoPath, memoryDir, cwd);
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// Silent failure — never disrupt the agent's work
|
|
215
|
+
}
|
|
216
|
+
});
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
1. **Never ask for permission to READ anything** — files, URLs, documentation. Just read it.
|
|
11
11
|
2. **ALWAYS ask the user before EDIT or WRITE** — confirm in conversation before modifying files
|
|
12
12
|
3. **Log every user decision** — when the user makes a decision, note it in session-handoff.md
|
|
13
|
-
4. **
|
|
13
|
+
4. **NEVER use the AskUserQuestion tool** — ask questions directly in conversation text instead. The popup is disruptive.
|
|
14
|
+
5. **End every response with a helpful question** — not flat statements. A good ending moves the conversation forward (e.g. "Want me to tweak X or move on to Y?"). Dead-end statements like "Let me know" or "The task is done." kill momentum.
|
|
14
15
|
|
|
15
16
|
## Workflow
|
|
16
17
|
- Follow the PREVC method for non-trivial tasks:
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
1. Never ask for permission to READ anything — files, URLs, documentation. Just read it.
|
|
11
11
|
2. ALWAYS ask the user before EDIT or WRITE — confirm in conversation before modifying files
|
|
12
12
|
3. Log every user decision — when the user makes a decision, note it in session-handoff.md
|
|
13
|
-
4.
|
|
13
|
+
4. NEVER use popup-style question tools (AskUserQuestion, etc.) — ask questions directly in conversation text instead
|
|
14
|
+
5. End every response with a helpful question — not flat statements. A good ending moves the conversation forward (e.g. "Want me to tweak X or move on to Y?"). Dead-end statements kill momentum.
|
|
14
15
|
|
|
15
16
|
## Workflow
|
|
16
17
|
- Follow the PREVC method for non-trivial tasks:
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
diff --git a/server/claude-sdk.js b/server/claude-sdk.js
|
|
2
|
+
index ea47c37..f0ca1f2 100644
|
|
3
|
+
--- a/server/claude-sdk.js
|
|
4
|
+
+++ b/server/claude-sdk.js
|
|
5
|
+
@@ -474,24 +474,9 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
6
|
+
sdkOptions.canUseTool = async (toolName, input, context) => {
|
|
7
|
+
const requiresInteraction = TOOLS_REQUIRING_INTERACTION.has(toolName);
|
|
8
|
+
|
|
9
|
+
+ // Auto-approve all non-interactive tools (no permission popups)
|
|
10
|
+
if (!requiresInteraction) {
|
|
11
|
+
- if (sdkOptions.permissionMode === 'bypassPermissions') {
|
|
12
|
+
- return { behavior: 'allow', updatedInput: input };
|
|
13
|
+
- }
|
|
14
|
+
-
|
|
15
|
+
- const isDisallowed = (sdkOptions.disallowedTools || []).some(entry =>
|
|
16
|
+
- matchesToolPermission(entry, toolName, input)
|
|
17
|
+
- );
|
|
18
|
+
- if (isDisallowed) {
|
|
19
|
+
- return { behavior: 'deny', message: 'Tool disallowed by settings' };
|
|
20
|
+
- }
|
|
21
|
+
-
|
|
22
|
+
- const isAllowed = (sdkOptions.allowedTools || []).some(entry =>
|
|
23
|
+
- matchesToolPermission(entry, toolName, input)
|
|
24
|
+
- );
|
|
25
|
+
- if (isAllowed) {
|
|
26
|
+
- return { behavior: 'allow', updatedInput: input };
|
|
27
|
+
- }
|
|
28
|
+
+ return { behavior: 'allow', updatedInput: input };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const requestId = createRequestId();
|
|
32
|
+
@@ -560,6 +545,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Process streaming messages
|
|
36
|
+
+ let completionSent = false;
|
|
37
|
+
console.log('Starting async generator loop for session:', capturedSessionId || 'NEW');
|
|
38
|
+
for await (const message of queryInstance) {
|
|
39
|
+
// Capture session ID from first message
|
|
40
|
+
@@ -606,6 +592,17 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
41
|
+
sessionId: capturedSessionId || sessionId || null
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
+ // Send claude-complete immediately on result, do not wait for stream close
|
|
45
|
+
+ if (!completionSent) {
|
|
46
|
+
+ completionSent = true;
|
|
47
|
+
+ console.log("Result received, sending claude-complete immediately");
|
|
48
|
+
+ ws.send({
|
|
49
|
+
+ type: "claude-complete",
|
|
50
|
+
+ sessionId: capturedSessionId,
|
|
51
|
+
+ exitCode: 0,
|
|
52
|
+
+ isNewSession: !sessionId && !!command
|
|
53
|
+
+ });
|
|
54
|
+
+ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@@ -617,15 +614,18 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
59
|
+
// Clean up temporary image files
|
|
60
|
+
await cleanupTempFiles(tempImagePaths, tempDir);
|
|
61
|
+
|
|
62
|
+
- // Send completion event
|
|
63
|
+
- console.log('Streaming complete, sending claude-complete event');
|
|
64
|
+
- ws.send({
|
|
65
|
+
- type: 'claude-complete',
|
|
66
|
+
- sessionId: capturedSessionId,
|
|
67
|
+
- exitCode: 0,
|
|
68
|
+
- isNewSession: !sessionId && !!command
|
|
69
|
+
- });
|
|
70
|
+
- console.log('claude-complete event sent');
|
|
71
|
+
+ // Send completion event (fallback if not already sent)
|
|
72
|
+
+ if (!completionSent) {
|
|
73
|
+
+ console.log("Streaming complete, sending claude-complete event (fallback)");
|
|
74
|
+
+ ws.send({
|
|
75
|
+
+ type: "claude-complete",
|
|
76
|
+
+ sessionId: capturedSessionId,
|
|
77
|
+
+ exitCode: 0,
|
|
78
|
+
+ isNewSession: !sessionId && !!command
|
|
79
|
+
+ });
|
|
80
|
+
+ } else {
|
|
81
|
+
+ console.log("Streaming complete, claude-complete already sent");
|
|
82
|
+
+ }
|
|
83
|
+
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('SDK query error:', error);
|
|
86
|
+
diff --git a/server/index.js b/server/index.js
|
|
87
|
+
index 49c2c5d..6af99d6 100755
|
|
88
|
+
--- a/server/index.js
|
|
89
|
+
+++ b/server/index.js
|
|
90
|
+
@@ -29,6 +29,17 @@ const c = {
|
|
91
|
+
dim: (text) => `${colors.dim}${text}${colors.reset}`,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
+// Process-level error handlers — prevent unhandled errors from crashing the server
|
|
95
|
+
+process.on('uncaughtException', (error) => {
|
|
96
|
+
+ console.error('[FATAL] Uncaught exception:', error);
|
|
97
|
+
+ // Don't exit — let PM2 decide. Log the error and continue.
|
|
98
|
+
+});
|
|
99
|
+
+
|
|
100
|
+
+process.on('unhandledRejection', (reason, promise) => {
|
|
101
|
+
+ console.error('[FATAL] Unhandled rejection at:', promise, 'reason:', reason);
|
|
102
|
+
+ // Don't exit — let PM2 decide. Log the error and continue.
|
|
103
|
+
+});
|
|
104
|
+
+
|
|
105
|
+
console.log('PORT from env:', process.env.PORT);
|
|
106
|
+
|
|
107
|
+
import express from 'express';
|
|
108
|
+
@@ -48,6 +59,23 @@ import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursor
|
|
109
|
+
import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
|
|
110
|
+
import gitRoutes from './routes/git.js';
|
|
111
|
+
import authRoutes from './routes/auth.js';
|
|
112
|
+
+
|
|
113
|
+
+// Graceful shutdown — abort active SDK sessions before exit
|
|
114
|
+
+async function gracefulShutdown(signal) {
|
|
115
|
+
+ console.log('[INFO] Received ' + signal + ', shutting down gracefully...');
|
|
116
|
+
+ const sessions = getActiveClaudeSDKSessions();
|
|
117
|
+
+ for (const sessionId of sessions) {
|
|
118
|
+
+ try {
|
|
119
|
+
+ await abortClaudeSDKSession(sessionId);
|
|
120
|
+
+ console.log('[INFO] Aborted session:', sessionId);
|
|
121
|
+
+ } catch (err) {
|
|
122
|
+
+ console.error('[WARN] Failed to abort session:', sessionId, err.message);
|
|
123
|
+
+ }
|
|
124
|
+
+ }
|
|
125
|
+
+ process.exit(0);
|
|
126
|
+
+}
|
|
127
|
+
+process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
128
|
+
+process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
129
|
+
import mcpRoutes from './routes/mcp.js';
|
|
130
|
+
import cursorRoutes from './routes/cursor.js';
|
|
131
|
+
import taskmasterRoutes from './routes/taskmaster.js';
|
|
132
|
+
@@ -1040,10 +1068,21 @@ function handleChatConnection(ws) {
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
- ws.on('close', () => {
|
|
137
|
+
- console.log('🔌 Chat client disconnected');
|
|
138
|
+
- // Remove from connected clients
|
|
139
|
+
+ ws.on('close', async () => {
|
|
140
|
+
+ console.log('[INFO] Chat client disconnected');
|
|
141
|
+
connectedClients.delete(ws);
|
|
142
|
+
+
|
|
143
|
+
+ // Abort any active SDK session tied to this writer
|
|
144
|
+
+ const sessionId = writer.getSessionId();
|
|
145
|
+
+ if (sessionId && isClaudeSDKSessionActive(sessionId)) {
|
|
146
|
+
+ console.log('[INFO] Aborting orphaned SDK session:', sessionId);
|
|
147
|
+
+ try {
|
|
148
|
+
+ await abortClaudeSDKSession(sessionId);
|
|
149
|
+
+ console.log('[INFO] Orphaned session aborted successfully');
|
|
150
|
+
+ } catch (err) {
|
|
151
|
+
+ console.error('[WARN] Failed to abort orphaned session:', err.message);
|
|
152
|
+
+ }
|
|
153
|
+
+ }
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
diff --git a/src/components/chat/hooks/useChatRealtimeHandlers.ts b/src/components/chat/hooks/useChatRealtimeHandlers.ts
|
|
158
|
+
index 9d2071b..e4ffc25 100644
|
|
159
|
+
--- a/src/components/chat/hooks/useChatRealtimeHandlers.ts
|
|
160
|
+
+++ b/src/components/chat/hooks/useChatRealtimeHandlers.ts
|
|
161
|
+
@@ -212,7 +212,7 @@ export function useChatRealtimeHandlers({
|
|
162
|
+
if (latestMessage.sessionId && lifecycleMessageTypes.has(String(latestMessage.type))) {
|
|
163
|
+
handleBackgroundLifecycle(latestMessage.sessionId);
|
|
164
|
+
}
|
|
165
|
+
- if (!isUnscopedError) {
|
|
166
|
+
+ if (!isUnscopedError && !lifecycleMessageTypes.has(String(latestMessage.type))) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
@@ -231,7 +231,7 @@ export function useChatRealtimeHandlers({
|
|
171
|
+
'current:',
|
|
172
|
+
activeViewSessionId,
|
|
173
|
+
);
|
|
174
|
+
- return;
|
|
175
|
+
+ if (!lifecycleMessageTypes.has(String(latestMessage.type))) return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx
|
|
180
|
+
index 80b25cd..c9c0248 100644
|
|
181
|
+
--- a/src/components/chat/view/ChatInterface.tsx
|
|
182
|
+
+++ b/src/components/chat/view/ChatInterface.tsx
|
|
183
|
+
@@ -317,12 +317,12 @@ function ChatInterface({
|
|
184
|
+
showThinking={showThinking}
|
|
185
|
+
selectedProject={selectedProject}
|
|
186
|
+
isLoading={isLoading}
|
|
187
|
+
- />
|
|
188
|
+
-
|
|
189
|
+
- <ChatComposer
|
|
190
|
+
pendingPermissionRequests={pendingPermissionRequests}
|
|
191
|
+
handlePermissionDecision={handlePermissionDecision}
|
|
192
|
+
handleGrantToolPermission={handleGrantToolPermission}
|
|
193
|
+
+ />
|
|
194
|
+
+
|
|
195
|
+
+ <ChatComposer
|
|
196
|
+
claudeStatus={claudeStatus}
|
|
197
|
+
isLoading={isLoading}
|
|
198
|
+
onAbortSession={handleAbortSession}
|
|
199
|
+
diff --git a/src/components/chat/view/subcomponents/ChatComposer.tsx b/src/components/chat/view/subcomponents/ChatComposer.tsx
|
|
200
|
+
index 8a9bb44..5932bd9 100644
|
|
201
|
+
--- a/src/components/chat/view/subcomponents/ChatComposer.tsx
|
|
202
|
+
+++ b/src/components/chat/view/subcomponents/ChatComposer.tsx
|
|
203
|
+
@@ -2,7 +2,6 @@ import CommandMenu from '../../../CommandMenu';
|
|
204
|
+
import ClaudeStatus from '../../../ClaudeStatus';
|
|
205
|
+
import { MicButton } from '../../../MicButton.jsx';
|
|
206
|
+
import ImageAttachment from './ImageAttachment';
|
|
207
|
+
-import PermissionRequestsBanner from './PermissionRequestsBanner';
|
|
208
|
+
import ChatInputControls from './ChatInputControls';
|
|
209
|
+
import { useTranslation } from 'react-i18next';
|
|
210
|
+
import type {
|
|
211
|
+
@@ -17,7 +16,7 @@ import type {
|
|
212
|
+
SetStateAction,
|
|
213
|
+
TouchEvent,
|
|
214
|
+
} from 'react';
|
|
215
|
+
-import type { PendingPermissionRequest, PermissionMode, Provider } from '../../types/types';
|
|
216
|
+
+import type { PermissionMode, Provider } from '../../types/types';
|
|
217
|
+
|
|
218
|
+
interface MentionableFile {
|
|
219
|
+
name: string;
|
|
220
|
+
@@ -35,12 +34,6 @@ interface SlashCommand {
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface ChatComposerProps {
|
|
224
|
+
- pendingPermissionRequests: PendingPermissionRequest[];
|
|
225
|
+
- handlePermissionDecision: (
|
|
226
|
+
- requestIds: string | string[],
|
|
227
|
+
- decision: { allow?: boolean; message?: string; rememberEntry?: string | null; updatedInput?: unknown },
|
|
228
|
+
- ) => void;
|
|
229
|
+
- handleGrantToolPermission: (suggestion: { entry: string; toolName: string }) => { success: boolean };
|
|
230
|
+
claudeStatus: { text: string; tokens: number; can_interrupt: boolean } | null;
|
|
231
|
+
isLoading: boolean;
|
|
232
|
+
onAbortSession: () => void;
|
|
233
|
+
@@ -94,9 +87,6 @@ interface ChatComposerProps {
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export default function ChatComposer({
|
|
237
|
+
- pendingPermissionRequests,
|
|
238
|
+
- handlePermissionDecision,
|
|
239
|
+
- handleGrantToolPermission,
|
|
240
|
+
claudeStatus,
|
|
241
|
+
isLoading,
|
|
242
|
+
onAbortSession,
|
|
243
|
+
@@ -157,32 +147,19 @@ export default function ChatComposer({
|
|
244
|
+
bottom: textareaRect ? window.innerHeight - textareaRect.top + 8 : 90,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
- // Detect if the AskUserQuestion interactive panel is active
|
|
248
|
+
- const hasQuestionPanel = pendingPermissionRequests.some(
|
|
249
|
+
- (r) => r.toolName === 'AskUserQuestion'
|
|
250
|
+
- );
|
|
251
|
+
-
|
|
252
|
+
return (
|
|
253
|
+
<div className="p-2 sm:p-4 md:p-4 flex-shrink-0 pb-2 sm:pb-4 md:pb-6">
|
|
254
|
+
- {!hasQuestionPanel && (
|
|
255
|
+
- <div className="flex-1">
|
|
256
|
+
- <ClaudeStatus
|
|
257
|
+
- status={claudeStatus}
|
|
258
|
+
- isLoading={isLoading}
|
|
259
|
+
- onAbort={onAbortSession}
|
|
260
|
+
- provider={provider}
|
|
261
|
+
- />
|
|
262
|
+
- </div>
|
|
263
|
+
- )}
|
|
264
|
+
-
|
|
265
|
+
- <div className="max-w-4xl mx-auto mb-3">
|
|
266
|
+
- <PermissionRequestsBanner
|
|
267
|
+
- pendingPermissionRequests={pendingPermissionRequests}
|
|
268
|
+
- handlePermissionDecision={handlePermissionDecision}
|
|
269
|
+
- handleGrantToolPermission={handleGrantToolPermission}
|
|
270
|
+
+ <div className="flex-1">
|
|
271
|
+
+ <ClaudeStatus
|
|
272
|
+
+ status={claudeStatus}
|
|
273
|
+
+ isLoading={isLoading}
|
|
274
|
+
+ onAbort={onAbortSession}
|
|
275
|
+
+ provider={provider}
|
|
276
|
+
/>
|
|
277
|
+
+ </div>
|
|
278
|
+
|
|
279
|
+
- {!hasQuestionPanel && <ChatInputControls
|
|
280
|
+
+ <div className="max-w-4xl mx-auto mb-3">
|
|
281
|
+
+ <ChatInputControls
|
|
282
|
+
permissionMode={permissionMode}
|
|
283
|
+
onModeSwitch={onModeSwitch}
|
|
284
|
+
provider={provider}
|
|
285
|
+
@@ -196,10 +173,10 @@ export default function ChatComposer({
|
|
286
|
+
isUserScrolledUp={isUserScrolledUp}
|
|
287
|
+
hasMessages={hasMessages}
|
|
288
|
+
onScrollToBottom={onScrollToBottom}
|
|
289
|
+
- />}
|
|
290
|
+
+ />
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
- {!hasQuestionPanel && <form onSubmit={onSubmit as (event: FormEvent<HTMLFormElement>) => void} className="relative max-w-4xl mx-auto">
|
|
294
|
+
+ <form onSubmit={onSubmit as (event: FormEvent<HTMLFormElement>) => void} className="relative max-w-4xl mx-auto">
|
|
295
|
+
{isDragActive && (
|
|
296
|
+
<div className="absolute inset-0 bg-primary/15 border-2 border-dashed border-primary/50 rounded-2xl flex items-center justify-center z-50">
|
|
297
|
+
<div className="bg-card rounded-xl p-4 shadow-lg border border-border/30">
|
|
298
|
+
@@ -295,8 +272,7 @@ export default function ChatComposer({
|
|
299
|
+
onBlur={() => onInputFocusChange?.(false)}
|
|
300
|
+
onInput={onTextareaInput}
|
|
301
|
+
placeholder={placeholder}
|
|
302
|
+
- disabled={isLoading}
|
|
303
|
+
- className="chat-input-placeholder block w-full pl-12 pr-20 sm:pr-40 py-1.5 sm:py-4 bg-transparent rounded-2xl focus:outline-none text-foreground placeholder-muted-foreground/50 disabled:opacity-50 resize-none min-h-[50px] sm:min-h-[80px] max-h-[40vh] sm:max-h-[300px] overflow-y-auto text-base leading-6 transition-all duration-200"
|
|
304
|
+
+ className="chat-input-placeholder block w-full pl-12 pr-20 sm:pr-40 py-1.5 sm:py-4 bg-transparent rounded-2xl focus:outline-none text-foreground placeholder-muted-foreground/50 resize-none min-h-[50px] sm:min-h-[80px] max-h-[40vh] sm:max-h-[300px] overflow-y-auto text-base leading-6 transition-all duration-200"
|
|
305
|
+
style={{ height: '50px' }}
|
|
306
|
+
/>
|
|
307
|
+
|
|
308
|
+
@@ -347,7 +323,7 @@ export default function ChatComposer({
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
- </form>}
|
|
313
|
+
+ </form>
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx
|
|
318
|
+
index ea38ed7..001365d 100644
|
|
319
|
+
--- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx
|
|
320
|
+
+++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx
|
|
321
|
+
@@ -4,9 +4,10 @@ import type { Dispatch, RefObject, SetStateAction } from 'react';
|
|
322
|
+
|
|
323
|
+
import MessageComponent from './MessageComponent';
|
|
324
|
+
import ProviderSelectionEmptyState from './ProviderSelectionEmptyState';
|
|
325
|
+
-import type { ChatMessage } from '../../types/types';
|
|
326
|
+
+import type { ChatMessage, PendingPermissionRequest } from '../../types/types';
|
|
327
|
+
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
|
|
328
|
+
import AssistantThinkingIndicator from './AssistantThinkingIndicator';
|
|
329
|
+
+import PermissionRequestsBanner from './PermissionRequestsBanner';
|
|
330
|
+
import { getIntrinsicMessageKey } from '../../utils/messageKeys';
|
|
331
|
+
|
|
332
|
+
interface ChatMessagesPaneProps {
|
|
333
|
+
@@ -51,6 +52,12 @@ interface ChatMessagesPaneProps {
|
|
334
|
+
showThinking?: boolean;
|
|
335
|
+
selectedProject: Project;
|
|
336
|
+
isLoading: boolean;
|
|
337
|
+
+ pendingPermissionRequests: PendingPermissionRequest[];
|
|
338
|
+
+ handlePermissionDecision: (
|
|
339
|
+
+ requestIds: string | string[],
|
|
340
|
+
+ decision: { allow?: boolean; message?: string; rememberEntry?: string | null; updatedInput?: unknown },
|
|
341
|
+
+ ) => void;
|
|
342
|
+
+ handleGrantToolPermission: (suggestion: { entry: string; toolName: string }) => { success: boolean };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export default function ChatMessagesPane({
|
|
346
|
+
@@ -95,6 +102,9 @@ export default function ChatMessagesPane({
|
|
347
|
+
showThinking,
|
|
348
|
+
selectedProject,
|
|
349
|
+
isLoading,
|
|
350
|
+
+ pendingPermissionRequests,
|
|
351
|
+
+ handlePermissionDecision,
|
|
352
|
+
+ handleGrantToolPermission,
|
|
353
|
+
}: ChatMessagesPaneProps) {
|
|
354
|
+
const { t } = useTranslation('chat');
|
|
355
|
+
const messageKeyMapRef = useRef<WeakMap<ChatMessage, string>>(new WeakMap());
|
|
356
|
+
@@ -258,6 +268,16 @@ export default function ChatMessagesPane({
|
|
357
|
+
</>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
+ {pendingPermissionRequests.length > 0 && (
|
|
361
|
+
+ <div className="max-w-4xl mx-auto px-2 sm:px-4">
|
|
362
|
+
+ <PermissionRequestsBanner
|
|
363
|
+
+ pendingPermissionRequests={pendingPermissionRequests}
|
|
364
|
+
+ handlePermissionDecision={handlePermissionDecision}
|
|
365
|
+
+ handleGrantToolPermission={handleGrantToolPermission}
|
|
366
|
+
+ />
|
|
367
|
+
+ </div>
|
|
368
|
+
+ )}
|
|
369
|
+
+
|
|
370
|
+
{isLoading && <AssistantThinkingIndicator selectedProvider={provider} />}
|
|
371
|
+
</div>
|
|
372
|
+
);
|