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.
@@ -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. **End responses with a helpful question** — keep the conversation moving forward
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. End responses with a helpful questionkeep the conversation moving forward
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
+ );