@agentprojectcontext/apx 1.32.0 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -1
- package/skills/apc-context/SKILL.md +5 -2
- package/skills/apx/SKILL.md +3 -3
- package/skills/apx-agency-agents/SKILL.md +5 -5
- package/skills/apx-agent/SKILL.md +7 -7
- package/skills/apx-mcp/SKILL.md +6 -4
- package/skills/apx-mcp-builder/SKILL.md +4 -7
- package/skills/apx-project/SKILL.md +4 -5
- package/skills/apx-routine/SKILL.md +14 -12
- package/skills/apx-runtime/SKILL.md +5 -3
- package/skills/apx-sessions/SKILL.md +5 -5
- package/skills/apx-skill-builder/SKILL.md +10 -6
- package/skills/apx-task/SKILL.md +8 -8
- package/skills/apx-telegram/SKILL.md +23 -7
- package/skills/apx-voice/SKILL.md +8 -6
- package/src/core/{agent-system.js → agent/build-agent-system.js} +10 -12
- package/src/core/agent/index.js +0 -2
- package/src/core/{agent-memory.js → agent/memory.js} +2 -2
- package/src/core/agent/model-router.js +21 -43
- package/src/core/agent/prompt-builder.js +17 -63
- package/src/core/agent/prompts/action-discipline.md +24 -0
- package/src/core/agent/prompts/channels/code.md +8 -12
- package/src/core/agent/prompts/channels/desktop.md +6 -4
- package/src/core/agent/prompts/channels/routine.md +10 -1
- package/src/core/agent/prompts/channels/telegram.md +10 -1
- package/src/core/agent/prompts/channels/web_code.md +20 -0
- package/src/core/agent/prompts/modes/voice.md +2 -2
- package/src/core/agent/prompts/super-agent-base.md +2 -2
- package/src/core/agent/run-agent.js +37 -35
- package/src/core/agent/runtime-bridge.js +42 -0
- package/src/core/agent/self-memory.js +19 -9
- package/src/core/agent/skills/catalog.js +65 -0
- package/src/core/agent/skills/index.js +6 -0
- package/src/{host/daemon/skills-loader.js → core/agent/skills/loader.js} +3 -3
- package/src/core/agent/skills/rag.js +91 -0
- package/src/core/agent/skills/trigger.js +71 -0
- package/src/{host/daemon → core/agent}/super-agent.js +5 -5
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/add-project.js +3 -4
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-agent.js +2 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-mcp.js +1 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-runtime.js +10 -11
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/create-task.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/discover-tools.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/edit-file.js +1 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/import-agent.js +4 -5
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-agents.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-skills.js +7 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-tasks.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-vault-agents.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/load-skill.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-agent-memory.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-self-memory.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/remember.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/run-shell.js +1 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-messages.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-sessions.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/send-telegram.js +0 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-identity.js +1 -3
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-permission-mode.js +1 -3
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/tail-messages.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/transcribe-audio.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/write-file.js +1 -2
- package/src/core/agent/tools/helpers.js +74 -0
- package/src/{host/daemon/super-agent-tools → core/agent/tools}/registry-bridge.js +3 -3
- package/src/{host/daemon/super-agent-tools/index.js → core/agent/tools/registry.js} +31 -32
- package/src/core/apc/agents-vault.js +37 -0
- package/src/core/{scaffold.js → apc/scaffold.js} +4 -5
- package/src/core/{config.js → config/index.js} +21 -27
- package/src/core/config/paths.js +32 -0
- package/src/core/constants/actors.js +8 -0
- package/src/core/constants/channels.js +19 -0
- package/src/core/constants/index.js +5 -0
- package/src/core/constants/permissions.js +17 -0
- package/src/core/constants/roles.js +9 -0
- package/src/core/engines/_streaming.js +63 -0
- package/src/core/engines/anthropic.js +11 -22
- package/src/core/engines/ollama.js +7 -16
- package/src/core/identity/index.js +8 -0
- package/src/core/{identity.js → identity/self.js} +5 -5
- package/src/core/{telegram-identity.js → identity/telegram.js} +1 -1
- package/src/core/logging.js +1 -1
- package/src/core/mascot.js +1 -1
- package/src/core/memory/active-threads.js +10 -10
- package/src/core/memory/broker.js +9 -9
- package/src/core/memory/compactor.js +2 -2
- package/src/core/memory/index.js +2 -2
- package/src/core/memory/indexer.js +1 -1
- package/src/core/{code-sessions-store.js → stores/code-sessions.js} +7 -8
- package/src/core/{messages-store.js → stores/messages.js} +6 -4
- package/src/core/stores/routine-memory.js +71 -0
- package/src/core/{routines-store.js → stores/routines.js} +1 -3
- package/src/core/stores/runtime-sessions.js +99 -0
- package/src/core/{tasks-store.js → stores/tasks.js} +3 -8
- package/src/core/update-check.js +1 -1
- package/src/core/util/ids.js +14 -0
- package/src/core/util/index.js +2 -0
- package/src/core/util/time.js +9 -0
- package/src/core/voice/tts.js +1 -1
- package/src/host/daemon/api/admin-config.js +4 -3
- package/src/host/daemon/api/admin.js +1 -1
- package/src/host/daemon/api/agents.js +4 -25
- package/src/host/daemon/api/artifacts.js +26 -1
- package/src/host/daemon/api/code.js +62 -17
- package/src/host/daemon/api/confirm.js +1 -1
- package/src/host/daemon/api/connections.js +2 -2
- package/src/host/daemon/api/conversations.js +2 -2
- package/src/host/daemon/api/deck.js +1 -1
- package/src/host/daemon/api/desktop.js +1 -1
- package/src/host/daemon/api/embeddings.js +4 -4
- package/src/host/daemon/api/engines.js +2 -2
- package/src/host/daemon/api/exec.js +20 -5
- package/src/host/daemon/api/identity.js +1 -1
- package/src/host/daemon/api/mcps.js +1 -1
- package/src/host/daemon/api/messages.js +1 -1
- package/src/host/daemon/api/runtimes.js +9 -8
- package/src/host/daemon/api/sessions-search.js +1 -1
- package/src/host/daemon/api/sessions.js +2 -2
- package/src/host/daemon/api/shared.js +5 -4
- package/src/host/daemon/api/skills.js +30 -0
- package/src/host/daemon/api/super-agent.js +29 -9
- package/src/host/daemon/api/tasks.js +2 -2
- package/src/host/daemon/api/telegram.js +1 -1
- package/src/host/daemon/api/tools.js +6 -6
- package/src/host/daemon/api/tts.js +2 -2
- package/src/host/daemon/api/voice.js +14 -12
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/compact.js +1 -1
- package/src/host/daemon/db.js +4 -4
- package/src/host/daemon/desktop-ws.js +1 -1
- package/src/host/daemon/index.js +4 -4
- package/src/host/daemon/plugins/{desktop.js → desktop/index.js} +11 -6
- package/src/host/daemon/plugins/index.js +2 -2
- package/src/host/daemon/plugins/{telegram.js → telegram/index.js} +52 -193
- package/src/host/daemon/plugins/telegram/media.js +162 -0
- package/src/host/daemon/projects-helpers.js +54 -0
- package/src/host/daemon/routines.js +28 -12
- package/src/host/daemon/smoke.js +2 -2
- package/src/host/daemon/token-store.js +1 -1
- package/src/host/daemon/transcription.js +2 -2
- package/src/host/daemon/wakeup.js +2 -2
- package/src/interfaces/cli/commands/agent.js +3 -3
- package/src/interfaces/cli/commands/command.js +1 -1
- package/src/interfaces/cli/commands/config.js +3 -2
- package/src/interfaces/cli/commands/desktop.js +1 -1
- package/src/interfaces/cli/commands/exec.js +2 -1
- package/src/interfaces/cli/commands/identity.js +2 -2
- package/src/interfaces/cli/commands/init.js +1 -1
- package/src/interfaces/cli/commands/mcp.js +1 -1
- package/src/interfaces/cli/commands/memory.js +2 -2
- package/src/interfaces/cli/commands/model.js +16 -6
- package/src/interfaces/cli/commands/project.js +1 -1
- package/src/interfaces/cli/commands/routine.js +58 -0
- package/src/interfaces/cli/commands/search.js +1 -1
- package/src/interfaces/cli/commands/session.js +4 -4
- package/src/interfaces/cli/commands/setup.js +4 -3
- package/src/interfaces/cli/commands/skills.js +25 -4
- package/src/interfaces/cli/commands/status.js +1 -1
- package/src/interfaces/cli/commands/sys.js +11 -4
- package/src/interfaces/cli/commands/update.js +1 -1
- package/src/interfaces/cli/index.js +4 -4
- package/src/interfaces/cli/postinstall.js +2 -2
- package/src/interfaces/mcp-server/index.js +1 -1
- package/src/interfaces/tui/component/prompt/index.tsx +3 -1
- package/src/interfaces/tui/context/sdk-apx.tsx +47 -7
- package/src/interfaces/tui/context/sync-apx.tsx +20 -2
- package/src/interfaces/tui/context/sync.tsx +2 -1
- package/src/interfaces/tui/routes/session/index.tsx +151 -136
- package/src/interfaces/tui/routes/session/sidebar-apx.tsx +37 -15
- package/src/interfaces/tui/run.ts +2 -0
- package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +1 -0
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +602 -0
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +6 -6
- package/src/interfaces/web/src/App.tsx +53 -32
- package/src/interfaces/web/src/components/RobyBubble.tsx +12 -6
- package/src/interfaces/web/src/components/UiSelect.tsx +13 -3
- package/src/interfaces/web/src/components/chat/SkillPicker.tsx +77 -0
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +253 -111
- package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +10 -8
- package/src/interfaces/web/src/components/code/CodeComposer.tsx +20 -17
- package/src/interfaces/web/src/components/code/CodeContextTab.tsx +43 -18
- package/src/interfaces/web/src/components/code/CodeFileTree.tsx +212 -0
- package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +121 -0
- package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeSessionList.tsx +30 -26
- package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +40 -21
- package/src/interfaces/web/src/components/code/CodeTerminal.tsx +140 -0
- package/src/interfaces/web/src/components/common/TabLayout.tsx +11 -7
- package/src/interfaces/web/src/components/common/TabNav.tsx +3 -3
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +4 -2
- package/src/interfaces/web/src/components/ui/chat-input.tsx +17 -6
- package/src/interfaces/web/src/hooks/useChat.ts +48 -2
- package/src/interfaces/web/src/hooks/useNavCollapseCtx.tsx +83 -0
- package/src/interfaces/web/src/hooks/usePersonaName.ts +11 -0
- package/src/interfaces/web/src/i18n/en.ts +7 -7
- package/src/interfaces/web/src/i18n/es.ts +8 -8
- package/src/interfaces/web/src/lib/api/agents.ts +1 -1
- package/src/interfaces/web/src/lib/api/artifacts.ts +10 -0
- package/src/interfaces/web/src/lib/api/code.ts +4 -2
- package/src/interfaces/web/src/lib/api/skills.ts +25 -0
- package/src/interfaces/web/src/lib/api.ts +1 -0
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +430 -86
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +5 -18
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -8
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +39 -40
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +16 -16
- package/src/skills/apc-context/SKILL.md +159 -0
- package/src/core/agent/ghost-guard.js +0 -24
- package/src/core/agent/prompts/channels/terminal.md +0 -16
- package/src/host/daemon/apc-runtime-context.js +0 -124
- package/src/host/daemon/super-agent-tools/helpers.js +0 -124
- package/src/host/daemon/tool-call-parser.js +0 -2
- package/src/interfaces/web/dist/assets/index-63P_ji1a.js +0 -571
- package/src/interfaces/web/dist/assets/index-63P_ji1a.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-DLWy6dYz.css +0 -1
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/ask-questions.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-files.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-mcps.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-projects.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-file.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-files.js +0 -0
- /package/src/core/agent/{pseudo-tools.js → tools/pseudo-tools.js} +0 -0
- /package/src/core/agent/{tool-call-parser.js → tools/tool-call-parser.js} +0 -0
- /package/src/core/{parser.js → apc/parser.js} +0 -0
- /package/src/core/{apc-skill-sync.js → apc/skill-sync.js} +0 -0
- /package/src/core/{artifacts-store.js → stores/artifacts.js} +0 -0
- /package/src/{host/daemon → core/stores}/engine-sessions.js +0 -0
- /package/src/core/{session-store.js → stores/sessions.js} +0 -0
- /package/src/host/daemon/plugins/{telegram-ask.js → telegram/ask.js} +0 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from "react";
|
|
2
|
+
import { File, Folder, FolderOpen, ChevronRight, ChevronsUpDown, RefreshCw } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Empty, Spinner } from "../ui";
|
|
5
|
+
import { Tip } from "../ui/tip";
|
|
6
|
+
import { http } from "../../lib/http";
|
|
7
|
+
|
|
8
|
+
interface FileNode {
|
|
9
|
+
name: string;
|
|
10
|
+
path: string; // relative path from project root
|
|
11
|
+
type: "file" | "dir";
|
|
12
|
+
children?: FileNode[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildTree(paths: string[]): FileNode[] {
|
|
16
|
+
const root: FileNode[] = [];
|
|
17
|
+
for (const p of paths) {
|
|
18
|
+
const parts = p.split("/").filter(Boolean);
|
|
19
|
+
let level = root;
|
|
20
|
+
let cumPath = "";
|
|
21
|
+
for (let i = 0; i < parts.length; i++) {
|
|
22
|
+
cumPath = cumPath ? `${cumPath}/${parts[i]}` : parts[i];
|
|
23
|
+
const isLast = i === parts.length - 1;
|
|
24
|
+
let node = level.find((n) => n.name === parts[i]);
|
|
25
|
+
if (!node) {
|
|
26
|
+
node = { name: parts[i], path: cumPath, type: isLast ? "file" : "dir", children: isLast ? undefined : [] };
|
|
27
|
+
level.push(node);
|
|
28
|
+
}
|
|
29
|
+
if (!isLast) level = node.children!;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Sort: dirs first, then files, both alphabetically
|
|
33
|
+
const sort = (nodes: FileNode[]): FileNode[] => {
|
|
34
|
+
nodes.forEach((n) => { if (n.children) n.children = sort(n.children); });
|
|
35
|
+
return nodes.sort((a, b) => {
|
|
36
|
+
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
37
|
+
return a.name.localeCompare(b.name);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
return sort(root);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function TreeNode({
|
|
44
|
+
node,
|
|
45
|
+
depth,
|
|
46
|
+
onOpenFile,
|
|
47
|
+
openDirs,
|
|
48
|
+
toggleDir,
|
|
49
|
+
}: {
|
|
50
|
+
node: FileNode;
|
|
51
|
+
depth: number;
|
|
52
|
+
onOpenFile: (path: string) => void;
|
|
53
|
+
openDirs: Set<string>;
|
|
54
|
+
toggleDir: (path: string) => void;
|
|
55
|
+
}) {
|
|
56
|
+
const isDir = node.type === "dir";
|
|
57
|
+
const open = isDir && openDirs.has(node.path);
|
|
58
|
+
return (
|
|
59
|
+
<li>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => isDir ? toggleDir(node.path) : onOpenFile(node.path)}
|
|
63
|
+
style={{ paddingLeft: `${depth * 12 + 6}px` }}
|
|
64
|
+
className={cn(
|
|
65
|
+
"flex w-full items-center gap-1.5 py-0.5 pr-2 text-left text-[11px] rounded transition-colors",
|
|
66
|
+
"hover:bg-accent/40",
|
|
67
|
+
isDir ? "text-foreground/80" : "text-foreground/70",
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{isDir ? (
|
|
71
|
+
<>
|
|
72
|
+
<ChevronRight className={cn("size-3 shrink-0 transition-transform", open && "rotate-90")} />
|
|
73
|
+
{open ? <FolderOpen className="size-3.5 shrink-0 text-amber-400" /> : <Folder className="size-3.5 shrink-0 text-amber-400" />}
|
|
74
|
+
</>
|
|
75
|
+
) : (
|
|
76
|
+
<>
|
|
77
|
+
<span className="size-3 shrink-0" />
|
|
78
|
+
<File className="size-3.5 shrink-0 text-sky-400" />
|
|
79
|
+
</>
|
|
80
|
+
)}
|
|
81
|
+
<span className="truncate">{node.name}</span>
|
|
82
|
+
</button>
|
|
83
|
+
{isDir && open && node.children && node.children.length > 0 && (
|
|
84
|
+
<ul>
|
|
85
|
+
{node.children.map((child) => (
|
|
86
|
+
<TreeNode
|
|
87
|
+
key={child.path}
|
|
88
|
+
node={child}
|
|
89
|
+
depth={depth + 1}
|
|
90
|
+
onOpenFile={onOpenFile}
|
|
91
|
+
openDirs={openDirs}
|
|
92
|
+
toggleDir={toggleDir}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
</ul>
|
|
96
|
+
)}
|
|
97
|
+
</li>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function CodeFileTree({
|
|
102
|
+
pid,
|
|
103
|
+
projectPath,
|
|
104
|
+
className,
|
|
105
|
+
onOpenFile,
|
|
106
|
+
}: {
|
|
107
|
+
pid: string;
|
|
108
|
+
projectPath?: string;
|
|
109
|
+
className?: string;
|
|
110
|
+
onOpenFile?: (path: string) => void;
|
|
111
|
+
}) {
|
|
112
|
+
const [files, setFiles] = useState<string[]>([]);
|
|
113
|
+
const [loading, setLoading] = useState(false);
|
|
114
|
+
const [loaded, setLoaded] = useState(false);
|
|
115
|
+
// Open-dir state lifted out of TreeNode so the parent can collapse all at once.
|
|
116
|
+
const [openDirs, setOpenDirs] = useState<Set<string>>(() => new Set());
|
|
117
|
+
|
|
118
|
+
const loadFiles = useCallback(async () => {
|
|
119
|
+
setLoading(true);
|
|
120
|
+
try {
|
|
121
|
+
const r = await http.post<{ ok: boolean; stdout: string; stderr: string }>(
|
|
122
|
+
"/run",
|
|
123
|
+
{
|
|
124
|
+
cmd: "find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/.claude/*' | sed 's|^\\./||' | sort | head -500",
|
|
125
|
+
project: pid,
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
const paths = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
129
|
+
setFiles(paths);
|
|
130
|
+
setLoaded(true);
|
|
131
|
+
} catch {
|
|
132
|
+
setLoaded(true);
|
|
133
|
+
} finally {
|
|
134
|
+
setLoading(false);
|
|
135
|
+
}
|
|
136
|
+
}, [pid]);
|
|
137
|
+
|
|
138
|
+
// Load on first render and whenever the project changes. Also reset the
|
|
139
|
+
// expanded set so a fresh project starts fully collapsed.
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
setOpenDirs(new Set());
|
|
142
|
+
void loadFiles();
|
|
143
|
+
}, [loadFiles]);
|
|
144
|
+
|
|
145
|
+
const toggleDir = useCallback((path: string) => {
|
|
146
|
+
setOpenDirs((prev) => {
|
|
147
|
+
const next = new Set(prev);
|
|
148
|
+
if (next.has(path)) next.delete(path);
|
|
149
|
+
else next.add(path);
|
|
150
|
+
return next;
|
|
151
|
+
});
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
const collapseAll = useCallback(() => {
|
|
155
|
+
setOpenDirs(new Set());
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
const tree = buildTree(files);
|
|
159
|
+
const anyOpen = openDirs.size > 0;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className={cn("flex h-full flex-col", className)} data-testid="code-file-tree">
|
|
163
|
+
{/* Header */}
|
|
164
|
+
<div className="flex shrink-0 items-center justify-between border-b border-border px-3 py-2">
|
|
165
|
+
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">Archivos</span>
|
|
166
|
+
<div className="flex items-center gap-0.5">
|
|
167
|
+
<Tip content="Colapsar todo">
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={collapseAll}
|
|
171
|
+
disabled={!anyOpen}
|
|
172
|
+
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
|
173
|
+
>
|
|
174
|
+
<ChevronsUpDown className="size-3" />
|
|
175
|
+
</button>
|
|
176
|
+
</Tip>
|
|
177
|
+
<Tip content="Recargar">
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={() => void loadFiles()}
|
|
181
|
+
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
182
|
+
>
|
|
183
|
+
{loading ? <Spinner size={12} /> : <RefreshCw className="size-3" />}
|
|
184
|
+
</button>
|
|
185
|
+
</Tip>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* File tree */}
|
|
190
|
+
<div className="min-h-0 flex-1 overflow-y-auto py-1">
|
|
191
|
+
{!loaded ? (
|
|
192
|
+
<div className="flex justify-center pt-6"><Spinner size={14} /></div>
|
|
193
|
+
) : tree.length === 0 ? (
|
|
194
|
+
<div className="p-3"><Empty>Sin archivos</Empty></div>
|
|
195
|
+
) : (
|
|
196
|
+
<ul>
|
|
197
|
+
{tree.map((node) => (
|
|
198
|
+
<TreeNode
|
|
199
|
+
key={node.path}
|
|
200
|
+
node={node}
|
|
201
|
+
depth={0}
|
|
202
|
+
onOpenFile={onOpenFile ?? (() => {})}
|
|
203
|
+
openDirs={openDirs}
|
|
204
|
+
toggleDir={toggleDir}
|
|
205
|
+
/>
|
|
206
|
+
))}
|
|
207
|
+
</ul>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Save, RotateCcw } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Spinner } from "../ui";
|
|
5
|
+
import { Tip } from "../ui/tip";
|
|
6
|
+
|
|
7
|
+
export function CodeFileViewer({
|
|
8
|
+
path,
|
|
9
|
+
content,
|
|
10
|
+
loading,
|
|
11
|
+
onSave,
|
|
12
|
+
}: {
|
|
13
|
+
path: string;
|
|
14
|
+
content: string;
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
/** When provided, the viewer becomes an editor: textarea + Save button. */
|
|
17
|
+
onSave?: (content: string) => Promise<void> | void;
|
|
18
|
+
}) {
|
|
19
|
+
const editable = typeof onSave === "function";
|
|
20
|
+
const [draft, setDraft] = useState(content);
|
|
21
|
+
const [saving, setSaving] = useState(false);
|
|
22
|
+
|
|
23
|
+
// Reset the draft whenever the upstream content changes (e.g. the file
|
|
24
|
+
// finished loading after the tab opened).
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setDraft(content);
|
|
27
|
+
}, [content]);
|
|
28
|
+
|
|
29
|
+
const dirty = editable && draft !== content;
|
|
30
|
+
|
|
31
|
+
const save = async () => {
|
|
32
|
+
if (!onSave || !dirty) return;
|
|
33
|
+
setSaving(true);
|
|
34
|
+
try {
|
|
35
|
+
await onSave(draft);
|
|
36
|
+
} finally {
|
|
37
|
+
setSaving(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex h-full min-h-0 flex-col bg-card/40" data-testid="code-file-viewer">
|
|
43
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-1.5">
|
|
44
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[11px] text-muted-foreground">
|
|
45
|
+
{path}
|
|
46
|
+
{dirty && <span className="ml-1 text-amber-400">•</span>}
|
|
47
|
+
</span>
|
|
48
|
+
{editable && (
|
|
49
|
+
<>
|
|
50
|
+
<Tip content="Descartar cambios">
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => setDraft(content)}
|
|
54
|
+
disabled={!dirty || saving}
|
|
55
|
+
className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40"
|
|
56
|
+
>
|
|
57
|
+
<RotateCcw className="size-3" />
|
|
58
|
+
Descartar
|
|
59
|
+
</button>
|
|
60
|
+
</Tip>
|
|
61
|
+
<Tip content="Guardar (Cmd/Ctrl+S)">
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={() => void save()}
|
|
65
|
+
disabled={!dirty || saving}
|
|
66
|
+
className={cn(
|
|
67
|
+
"inline-flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-medium transition-colors",
|
|
68
|
+
dirty && !saving
|
|
69
|
+
? "bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/25 dark:text-emerald-300"
|
|
70
|
+
: "bg-muted text-muted-foreground",
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{saving ? <Spinner size={10} /> : <Save className="size-3" />}
|
|
74
|
+
Guardar
|
|
75
|
+
</button>
|
|
76
|
+
</Tip>
|
|
77
|
+
</>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{loading ? (
|
|
82
|
+
<div className="flex flex-1 items-center justify-center">
|
|
83
|
+
<Spinner size={16} />
|
|
84
|
+
</div>
|
|
85
|
+
) : editable ? (
|
|
86
|
+
<textarea
|
|
87
|
+
value={draft}
|
|
88
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
89
|
+
onKeyDown={(e) => {
|
|
90
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
void save();
|
|
93
|
+
}
|
|
94
|
+
}}
|
|
95
|
+
className="min-h-0 flex-1 resize-none bg-transparent p-3 font-mono text-[12px] leading-[1.6] text-foreground/90 outline-none"
|
|
96
|
+
spellCheck={false}
|
|
97
|
+
/>
|
|
98
|
+
) : (
|
|
99
|
+
<div className="min-h-0 flex-1 overflow-auto">
|
|
100
|
+
<table className="w-full border-collapse font-mono text-[12px] leading-[1.6]">
|
|
101
|
+
<tbody>
|
|
102
|
+
{content.split("\n").map((line, i) => (
|
|
103
|
+
<tr key={i} className="hover:bg-accent/20">
|
|
104
|
+
<td
|
|
105
|
+
className="w-12 select-none border-r border-border/30 px-3 py-0 text-right align-top text-[10px] text-muted-foreground/40"
|
|
106
|
+
aria-hidden="true"
|
|
107
|
+
>
|
|
108
|
+
{i + 1}
|
|
109
|
+
</td>
|
|
110
|
+
<td className="px-4 py-0 align-top text-foreground/90 whitespace-pre">
|
|
111
|
+
{line || " "}
|
|
112
|
+
</td>
|
|
113
|
+
</tr>
|
|
114
|
+
))}
|
|
115
|
+
</tbody>
|
|
116
|
+
</table>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -26,7 +26,7 @@ export function CodeProjectPicker({ projects, value, onChange, disabled }: Props
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
|
-
<div className="w-
|
|
29
|
+
<div className="w-full" data-testid="code-project-select">
|
|
30
30
|
<UiSelect
|
|
31
31
|
value={value}
|
|
32
32
|
onChange={onChange}
|
|
@@ -2,6 +2,7 @@ import { Plus, MessageSquare, Trash2, Pencil } from "lucide-react";
|
|
|
2
2
|
import { cn } from "../../lib/cn";
|
|
3
3
|
import { t } from "../../i18n";
|
|
4
4
|
import { Empty } from "../ui";
|
|
5
|
+
import { Tip } from "../ui/tip";
|
|
5
6
|
import type { CodeSessionRow } from "../../lib/api/code";
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
@@ -30,16 +31,17 @@ export function CodeSessionList({
|
|
|
30
31
|
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
31
32
|
{t("code_module.sessions")}
|
|
32
33
|
</span>
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
<Tip content={t("code_module.new_session")}>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
onClick={onCreate}
|
|
38
|
+
disabled={busy}
|
|
39
|
+
data-testid="code-new-session"
|
|
40
|
+
className="flex items-center gap-1 rounded-md border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:opacity-50"
|
|
41
|
+
>
|
|
42
|
+
<Plus className="size-3" /> {t("code_module.new_session")}
|
|
43
|
+
</button>
|
|
44
|
+
</Tip>
|
|
43
45
|
</div>
|
|
44
46
|
|
|
45
47
|
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-2">
|
|
@@ -70,22 +72,24 @@ export function CodeSessionList({
|
|
|
70
72
|
</span>
|
|
71
73
|
</button>
|
|
72
74
|
<div className="absolute right-1 top-1 hidden items-center gap-0.5 group-hover/item:flex">
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
<Tip content={t("code_module.rename")}>
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={() => onRename(s.id, s.title)}
|
|
79
|
+
className="rounded p-1 text-muted-foreground hover:bg-background hover:text-foreground"
|
|
80
|
+
>
|
|
81
|
+
<Pencil className="size-3" />
|
|
82
|
+
</button>
|
|
83
|
+
</Tip>
|
|
84
|
+
<Tip content={t("code_module.delete")}>
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={() => onDelete(s.id)}
|
|
88
|
+
className="rounded p-1 text-muted-foreground hover:bg-background hover:text-rose-500"
|
|
89
|
+
>
|
|
90
|
+
<Trash2 className="size-3" />
|
|
91
|
+
</button>
|
|
92
|
+
</Tip>
|
|
89
93
|
</div>
|
|
90
94
|
</li>
|
|
91
95
|
))}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import { Gauge, GitCompare, Package } from "lucide-react";
|
|
2
3
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
|
|
4
|
+
import { Tip } from "../ui/tip";
|
|
3
5
|
import { t } from "../../i18n";
|
|
4
6
|
import { CodeContextTab } from "./CodeContextTab";
|
|
5
7
|
import { CodeChangesTab } from "./CodeChangesTab";
|
|
@@ -12,40 +14,57 @@ interface Props {
|
|
|
12
14
|
changes: CodeChanges | undefined;
|
|
13
15
|
changesLoading: boolean;
|
|
14
16
|
onRefreshChanges: () => void;
|
|
17
|
+
session?: { title: string; mode: string; createdAt: string; updatedAt: string; agentSlug: string | null } | null;
|
|
18
|
+
onRunInTerminal?: (cmd: string) => void;
|
|
19
|
+
onEditArtifact?: (name: string) => void;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const TABS = [
|
|
23
|
+
{ value: "context", icon: Gauge, label: "tab_context" },
|
|
24
|
+
{ value: "changes", icon: GitCompare, label: "tab_changes" },
|
|
25
|
+
{ value: "artifacts", icon: Package, label: "tab_artifacts" },
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export function CodeSidePanel({ pid, turns, changes, changesLoading, onRefreshChanges, session, onRunInTerminal, onEditArtifact }: Props) {
|
|
29
|
+
const [active, setActive] = useState<string>("context");
|
|
20
30
|
const changeCount = changes?.files.length || 0;
|
|
31
|
+
|
|
21
32
|
return (
|
|
22
|
-
<Tabs
|
|
23
|
-
<div className="shrink-0 border-b border-border px-
|
|
33
|
+
<Tabs value={active} onValueChange={setActive} className="flex h-full flex-col gap-0" data-testid="code-side-panel">
|
|
34
|
+
<div className="shrink-0 border-b border-border px-2 py-2">
|
|
24
35
|
<TabsList variant="line" className="w-full">
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
{TABS.map(({ value, icon: Icon, label }) => {
|
|
37
|
+
const isActive = active === value;
|
|
38
|
+
const fullLabel = t(`code_module.${label}` as never);
|
|
39
|
+
return (
|
|
40
|
+
<Tip key={value} content={fullLabel}>
|
|
41
|
+
<TabsTrigger
|
|
42
|
+
value={value}
|
|
43
|
+
className={isActive ? "flex-1 min-w-0" : "w-8 shrink-0"}
|
|
44
|
+
>
|
|
45
|
+
<Icon className="size-3.5 shrink-0" />
|
|
46
|
+
{isActive && (
|
|
47
|
+
<span className="truncate text-xs">{fullLabel}</span>
|
|
48
|
+
)}
|
|
49
|
+
{value === "changes" && changeCount > 0 && (
|
|
50
|
+
<span className="ml-0.5 rounded-full bg-muted px-1 text-[10px] text-muted-foreground leading-none py-0.5">
|
|
51
|
+
{changeCount}
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</TabsTrigger>
|
|
55
|
+
</Tip>
|
|
56
|
+
);
|
|
57
|
+
})}
|
|
39
58
|
</TabsList>
|
|
40
59
|
</div>
|
|
41
60
|
<TabsContent value="context" className="min-h-0 flex-1 overflow-y-auto">
|
|
42
|
-
<CodeContextTab turns={turns} />
|
|
61
|
+
<CodeContextTab turns={turns} session={session} />
|
|
43
62
|
</TabsContent>
|
|
44
63
|
<TabsContent value="changes" className="min-h-0 flex-1 overflow-hidden">
|
|
45
64
|
<CodeChangesTab changes={changes} loading={changesLoading} onRefresh={onRefreshChanges} />
|
|
46
65
|
</TabsContent>
|
|
47
66
|
<TabsContent value="artifacts" className="min-h-0 flex-1 overflow-hidden">
|
|
48
|
-
<CodeArtifactsTab pid={pid} />
|
|
67
|
+
<CodeArtifactsTab pid={pid} onRunInTerminal={onRunInTerminal} onEditArtifact={onEditArtifact} />
|
|
49
68
|
</TabsContent>
|
|
50
69
|
</Tabs>
|
|
51
70
|
);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { Terminal as TerminalIcon, Eraser, X } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Tip } from "../ui/tip";
|
|
5
|
+
import { http } from "../../lib/http";
|
|
6
|
+
|
|
7
|
+
interface Line {
|
|
8
|
+
type: "cmd" | "out" | "err";
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CodeTerminal({
|
|
13
|
+
pid,
|
|
14
|
+
className,
|
|
15
|
+
initCmd,
|
|
16
|
+
onClose,
|
|
17
|
+
}: {
|
|
18
|
+
pid: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
initCmd?: string;
|
|
21
|
+
/** Click handler for the header × button. Closes the terminal panel. */
|
|
22
|
+
onClose?: () => void;
|
|
23
|
+
}) {
|
|
24
|
+
const [lines, setLines] = useState<Line[]>([]);
|
|
25
|
+
const [input, setInput] = useState("");
|
|
26
|
+
const [busy, setBusy] = useState(false);
|
|
27
|
+
const [history, setHistory] = useState<string[]>([]);
|
|
28
|
+
const [histIdx, setHistIdx] = useState(-1);
|
|
29
|
+
const bottomRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
34
|
+
}, [lines]);
|
|
35
|
+
|
|
36
|
+
// Pre-fill input when a command is pushed from parent (e.g. artifact run).
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!initCmd) return;
|
|
39
|
+
setInput(initCmd);
|
|
40
|
+
setTimeout(() => inputRef.current?.focus(), 50);
|
|
41
|
+
}, [initCmd]);
|
|
42
|
+
|
|
43
|
+
const run = async (cmd: string) => {
|
|
44
|
+
const trimmed = cmd.trim();
|
|
45
|
+
if (!trimmed) return;
|
|
46
|
+
setHistory((h) => [trimmed, ...h.slice(0, 49)]);
|
|
47
|
+
setHistIdx(-1);
|
|
48
|
+
setLines((l) => [...l, { type: "cmd", text: `$ ${trimmed}` }]);
|
|
49
|
+
setBusy(true);
|
|
50
|
+
try {
|
|
51
|
+
const r = await http.post<{ ok: boolean; stdout: string; stderr: string; exit_code: number; cwd: string }>(
|
|
52
|
+
"/run",
|
|
53
|
+
{ cmd: trimmed, project: pid },
|
|
54
|
+
);
|
|
55
|
+
if (r.stdout) setLines((l) => [...l, { type: "out", text: r.stdout }]);
|
|
56
|
+
if (r.stderr) setLines((l) => [...l, { type: "err", text: r.stderr }]);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
setLines((l) => [...l, { type: "err", text: String((e as Error).message) }]);
|
|
59
|
+
} finally {
|
|
60
|
+
setBusy(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
65
|
+
if (e.key === "Enter") {
|
|
66
|
+
void run(input);
|
|
67
|
+
setInput("");
|
|
68
|
+
} else if (e.key === "ArrowUp") {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
const next = Math.min(histIdx + 1, history.length - 1);
|
|
71
|
+
setHistIdx(next);
|
|
72
|
+
setInput(history[next] ?? "");
|
|
73
|
+
} else if (e.key === "ArrowDown") {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
const next = Math.max(histIdx - 1, -1);
|
|
76
|
+
setHistIdx(next);
|
|
77
|
+
setInput(next === -1 ? "" : (history[next] ?? ""));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className={cn("flex h-full min-h-0 flex-col bg-card/60", className)} data-testid="code-terminal">
|
|
83
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-1">
|
|
84
|
+
<TerminalIcon className="size-3 text-muted-foreground" />
|
|
85
|
+
<span className="flex-1 text-[11px] text-muted-foreground">Terminal</span>
|
|
86
|
+
<Tip content="Limpiar">
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={() => setLines([])}
|
|
90
|
+
className="rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
91
|
+
>
|
|
92
|
+
<Eraser className="size-3" />
|
|
93
|
+
</button>
|
|
94
|
+
</Tip>
|
|
95
|
+
<Tip content="Cerrar terminal">
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={() => onClose?.()}
|
|
99
|
+
className="rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
100
|
+
>
|
|
101
|
+
<X className="size-3" />
|
|
102
|
+
</button>
|
|
103
|
+
</Tip>
|
|
104
|
+
</div>
|
|
105
|
+
<div
|
|
106
|
+
className="min-h-0 flex-1 overflow-y-auto px-3 py-1 font-mono text-[11px] leading-snug cursor-text"
|
|
107
|
+
onClick={() => inputRef.current?.focus()}
|
|
108
|
+
>
|
|
109
|
+
{lines.map((l, i) => (
|
|
110
|
+
<div
|
|
111
|
+
key={i}
|
|
112
|
+
className={cn(
|
|
113
|
+
"whitespace-pre-wrap break-all",
|
|
114
|
+
l.type === "cmd" && "text-emerald-400",
|
|
115
|
+
l.type === "err" && "text-rose-400",
|
|
116
|
+
l.type === "out" && "text-foreground/90",
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
{l.text}
|
|
120
|
+
</div>
|
|
121
|
+
))}
|
|
122
|
+
<div ref={bottomRef} />
|
|
123
|
+
</div>
|
|
124
|
+
<div className="flex shrink-0 items-center border-t border-border px-3 py-1">
|
|
125
|
+
<span className="mr-2 text-[11px] text-emerald-400 font-mono">$</span>
|
|
126
|
+
<input
|
|
127
|
+
ref={inputRef}
|
|
128
|
+
value={input}
|
|
129
|
+
onChange={(e) => setInput(e.target.value)}
|
|
130
|
+
onKeyDown={onKey}
|
|
131
|
+
disabled={busy}
|
|
132
|
+
placeholder={busy ? "ejecutando…" : "comando…"}
|
|
133
|
+
className="flex-1 bg-transparent font-mono text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 disabled:opacity-50"
|
|
134
|
+
spellCheck={false}
|
|
135
|
+
autoComplete="off"
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|