karajan-code 4.23.0 → 4.25.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/README.md +8 -0
- package/package.json +1 -1
- package/packages/hu-board/package.json +5 -2
- package/packages/hu-board/public/index.html +5 -0
- package/packages/hu-board/public/utils/board-view.js +14 -13
- package/packages/hu-board/public/utils/command-launcher.js +42 -6
- package/packages/hu-board/public/utils/log-panel.js +4 -4
- package/packages/hu-board/public/utils/maggle-mode.js +127 -0
- package/packages/hu-board/public/utils/modals.js +11 -2
- package/packages/hu-board/public/utils/project-picker-view.js +3 -3
- package/packages/hu-board/public/utils/terminal-panel.js +132 -0
- package/packages/hu-board/src/server.js +25 -0
- package/packages/hu-board/src/sync.js +44 -6
- package/packages/hu-board/src/terminal-wire.js +96 -0
- package/packages/hu-board/src/terminal.js +122 -0
- package/scripts/install-binary.ps1 +12 -3
- package/scripts/install-binary.sh +13 -3
- package/src/cli/advanced-commands.js +1 -0
- package/src/cli/register-meta.js +13 -0
- package/src/commands/board.js +19 -4
- package/src/commands/go.js +139 -0
- package/src/steward/phantom-coverage.js +44 -7
package/README.md
CHANGED
|
@@ -59,6 +59,14 @@ kj init && kj env install && kj harden && kj review --install-gate
|
|
|
59
59
|
git config core.hooksPath .karajan/hooks
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
Already installed and just want to work — even with no computing background? One command:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
kj go
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
It detects your agent (Claude Code or Codex; asks which one only if you have both), prepares the project silently the first time, opens the board in your browser, and drops you into a conversation that already follows the method. Your agent account and login stay yours — kj never touches credentials. Prefer everything in ONE browser window? `kj go --window` embeds the agent's real terminal inside the board (loopback-only, single-session token).
|
|
69
|
+
|
|
62
70
|
Requires git and at least one AI agent CLI — two enables cross-AI review; three enables arbitration. The npm route is `npm install -g @karajan-family/code` (published as `karajan-code` before joining the scope; the legacy name still installs the same versions). All install routes (npm, binaries, brew, Python wrapper) in the [install docs](https://karajancode.com/docs/v4/install/).
|
|
63
71
|
|
|
64
72
|
## The daily loop
|
package/package.json
CHANGED
|
@@ -13,13 +13,16 @@
|
|
|
13
13
|
"test": "vitest run"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"
|
|
16
|
+
"@xterm/xterm": "^6.0.0",
|
|
17
17
|
"better-sqlite3": "^12.10.0",
|
|
18
18
|
"chokidar": "^5.0.0",
|
|
19
19
|
"express": "^5.1.0",
|
|
20
20
|
"express-rate-limit": "^8.5.0",
|
|
21
21
|
"helmet": "^8.1.0",
|
|
22
|
-
"js-yaml": "^4.2.0"
|
|
22
|
+
"js-yaml": "^4.2.0",
|
|
23
|
+
"karajan-core": "*",
|
|
24
|
+
"node-pty": "^1.1.0",
|
|
25
|
+
"ws": "^8.21.3"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
28
|
"supertest": "^7.1.0",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Karajan HU Board</title>
|
|
7
7
|
<link rel="stylesheet" href="/styles.css">
|
|
8
|
+
<link rel="stylesheet" href="/vendor/xterm/css/xterm.css">
|
|
8
9
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%237c3aed'/><text x='16' y='22' text-anchor='middle' fill='white' font-size='16' font-family='sans-serif' font-weight='bold'>K</text></svg>">
|
|
9
10
|
</head>
|
|
10
11
|
<body>
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
<button class="control-btn" id="help-btn" title="What does each view do? Quick reference for Board / Graph / Dashboard / Sessions / Pipeline.">?</button>
|
|
36
37
|
<button class="control-btn" id="config-btn" title="Configurar Karajan (modelos, agentes, tope de iteraciones, …)">⚙</button>
|
|
37
38
|
<button class="control-btn" id="commands-btn" title="Launch a Karajan CLI command (kj plan, kj architect, kj clean…)">⚡</button>
|
|
39
|
+
<button class="control-btn" id="conversation-btn" title="Conversación con tu agente — la terminal real, dentro del board (ADR 0008)" onclick="openConversationPanel()">💬</button>
|
|
38
40
|
<button class="control-btn" id="restart-btn" title="Restart the HU Board server (preserves URL, the page reconnects automatically)">🔁</button>
|
|
39
41
|
<button class="control-btn" id="sync-btn" title="Re-scan disk for new batches">🔄</button>
|
|
40
42
|
<button class="control-btn" id="force-refresh-btn" title="Force a clean reload — wipes browser caches and reloads. Use when the UI looks stale and a normal refresh isn't fixing it." onclick="window.forceRefresh()">🧹</button>
|
|
@@ -53,6 +55,7 @@
|
|
|
53
55
|
<div class="modal" id="modal-content"></div>
|
|
54
56
|
</div>
|
|
55
57
|
|
|
58
|
+
<script src="/utils/maggle-mode.js"></script>
|
|
56
59
|
<script src="/utils/formatters.js"></script>
|
|
57
60
|
<script src="/utils/modals.js"></script>
|
|
58
61
|
<script src="/utils/api.js"></script>
|
|
@@ -66,6 +69,8 @@
|
|
|
66
69
|
<script src="/utils/preflight-view.js"></script>
|
|
67
70
|
<script src="/utils/plan-rollup-view.js"></script>
|
|
68
71
|
<script src="/utils/log-panel.js"></script>
|
|
72
|
+
<script src="/vendor/xterm/lib/xterm.js"></script>
|
|
73
|
+
<script src="/utils/terminal-panel.js"></script>
|
|
69
74
|
<script src="/utils/hu-actions.js"></script>
|
|
70
75
|
<script src="/utils/project-actions.js"></script>
|
|
71
76
|
<script src="/utils/story-edit-form.js"></script>
|
|
@@ -87,48 +87,49 @@ async function renderBoard() {
|
|
|
87
87
|
|
|
88
88
|
// KJC-TSK-0403: 3 canonical lanes. Failed eliminado — HUs con
|
|
89
89
|
// result=fail aparecen en Pending con badge ✗.
|
|
90
|
+
// Maggle mode (KJC-TSK-0810): plain label first, jargon as tooltip.
|
|
90
91
|
const visibleColumns = [
|
|
91
|
-
{ title: 'Pending', cls: 'pending', rows: columns.pending },
|
|
92
|
-
{ title: 'Running', cls: 'running', rows: columns.running },
|
|
93
|
-
{ title: 'Done', cls: 'done', rows: columns.done },
|
|
92
|
+
{ title: maggleText('column.pending', 'Pending'), cls: 'pending', rows: columns.pending },
|
|
93
|
+
{ title: maggleText('column.running', 'Running'), cls: 'running', rows: columns.running },
|
|
94
|
+
{ title: maggleText('column.done', 'Done'), cls: 'done', rows: columns.done },
|
|
94
95
|
];
|
|
95
96
|
|
|
96
97
|
app.innerHTML = `
|
|
97
98
|
<div class="section-header">
|
|
98
|
-
<span class="section-header__title" title="${selectedProject ? esc(selectedProject) : ''}"
|
|
99
|
+
<span class="section-header__title" title="${selectedProject ? esc(selectedProject) : ''}">${maggleText('board.title', 'Story Board')}${selectedProject ? ` - ${esc(projectDisplayName)}` : ''}</span>
|
|
99
100
|
${selectedProject ? `
|
|
100
101
|
<button class="control-btn project-rename-btn"
|
|
101
102
|
style="background:transparent;border:none;color:var(--text-muted);cursor:pointer;font-size:0.9rem;padding:2px 6px;"
|
|
102
103
|
title="Renombrar este proyecto"
|
|
103
104
|
onclick="event.stopPropagation(); window.renameProjectModal('${esc(selectedProject)}', '${esc(projectDisplayName.replace(/'/g, '''))}');">✎</button>
|
|
104
105
|
` : ''}
|
|
105
|
-
<span class="section-header__count">${stories.length} stories</span>
|
|
106
|
+
<span class="section-header__count" title="stories">${stories.length} ${maggleText('board.stories', 'stories')}</span>
|
|
106
107
|
${costSummary ? `<span class="section-header__cost" title="${esc(costSummary.tooltip)}">💵 ${esc(costSummary.label)}</span>` : ''}
|
|
107
108
|
${cacheSummary ? `<span class="section-header__cache" title="${esc(cacheSummary.tooltip)}">${esc(cacheSummary.label)}</span>` : ''}
|
|
108
109
|
${isRunning ? `
|
|
109
110
|
<button id="running-badge-btn" class="section-header__badge"
|
|
110
111
|
style="margin-left:auto;padding:4px 10px;font-size:0.8rem;background:var(--color-yellow,#eab308);color:#000;border-radius:var(--radius-sm);font-weight:600;border:none;cursor:pointer;"
|
|
111
112
|
title="Abrir el log de la HU en marcha">
|
|
112
|
-
⚙ ${runningCount} running…
|
|
113
|
+
⚙ ${runningCount} ${maggleText('board.running', 'running')}…
|
|
113
114
|
</button>
|
|
114
115
|
<button id="stop-run-btn" class="control-btn"
|
|
115
116
|
style="padding:4px 10px;font-size:0.8rem;background:var(--color-red,#ef4444);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-weight:600;"
|
|
116
|
-
title="Abortar todos los kj run en marcha de este proyecto (SIGTERM con escalado a SIGKILL tras 5s)">
|
|
117
|
-
⏹ Stop
|
|
117
|
+
title="${maggleText('board.stopTitle', 'Abortar todos los kj run en marcha de este proyecto (SIGTERM con escalado a SIGKILL tras 5s)')}">
|
|
118
|
+
${maggleText('board.stop', '⏹ Stop')}
|
|
118
119
|
</button>
|
|
119
120
|
` : ''}
|
|
120
121
|
${lastOpenedLog ? `
|
|
121
122
|
<button class="control-btn" id="view-log-btn"
|
|
122
123
|
style="${isRunning ? '' : 'margin-left:auto;'}padding:6px 12px;font-size:0.85rem;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);cursor:pointer;"
|
|
123
124
|
title="Re-open the log of the most recent run/command">
|
|
124
|
-
📜 View log
|
|
125
|
+
${maggleText('board.viewLog', '📜 View log')}
|
|
125
126
|
</button>
|
|
126
127
|
` : ''}
|
|
127
128
|
${canRun ? `
|
|
128
129
|
<button class="control-btn" id="run-plan-btn"
|
|
129
130
|
style="margin-left:auto;padding:6px 14px;font-size:0.9rem;background:var(--color-green);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-weight:600;"
|
|
130
|
-
title="Launch kj run --plan over every plan in this project">
|
|
131
|
-
▶ Run plan (${awaitingCount} HU${awaitingCount === 1 ? '' : 's'})
|
|
131
|
+
title="${maggleText('board.runTitle', 'Launch kj run --plan over every plan in this project')}">
|
|
132
|
+
${maggleText('board.run', '▶ Run plan')} (${awaitingCount} ${maggleText('board.story', 'HU')}${awaitingCount === 1 ? '' : 's'})
|
|
132
133
|
</button>
|
|
133
134
|
` : ''}
|
|
134
135
|
</div>
|
|
@@ -138,7 +139,7 @@ async function renderBoard() {
|
|
|
138
139
|
</div>
|
|
139
140
|
</div>
|
|
140
141
|
<div id="rag-panel" class="rag-panel" style="margin:8px 0;display:flex;gap:8px;align-items:flex-start;flex-wrap:wrap;">
|
|
141
|
-
<input id="rag-query" type="text" placeholder="🔍 RAG search: ask anything about this project's plans / onboarding / code…"
|
|
142
|
+
<input id="rag-query" type="text" placeholder="${maggleText('board.ragPlaceholder', "🔍 RAG search: ask anything about this project's plans / onboarding / code…")}"
|
|
142
143
|
style="flex:1;min-width:260px;padding:6px 10px;font-size:0.85rem;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);" />
|
|
143
144
|
<select id="rag-scope" style="padding:6px 10px;font-size:0.85rem;background:var(--bg-primary);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);">
|
|
144
145
|
<option value="all">All</option><option value="plans">Plans</option><option value="onboarding">Onboarding</option><option value="code">Code</option>
|
|
@@ -305,7 +306,7 @@ function renderKanbanColumn(title, cssClass, stories) {
|
|
|
305
306
|
return `
|
|
306
307
|
<div class="kanban__column kanban__column--${cssClass}" data-column="${cssClass}"${stories.length === 0 ? ' style="opacity:0.55"' : ''}>
|
|
307
308
|
<div class="kanban__column-header">
|
|
308
|
-
<span class="kanban__column-title">${title}</span>
|
|
309
|
+
<span class="kanban__column-title" title="${cssClass}">${title}</span>
|
|
309
310
|
<span class="kanban__column-count" data-column-count>${stories.length}</span>
|
|
310
311
|
</div>
|
|
311
312
|
${stories.map(renderStoryCard).join('')}
|
|
@@ -60,10 +60,25 @@ async function showCommandLauncher() {
|
|
|
60
60
|
},
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
// Maggle mode (KJC-TSK-0810 AC2): the frequent action leads in plain
|
|
64
|
+
// language — one textarea, clear promise of what will happen. The full
|
|
65
|
+
// technical launcher (file paths, architect, clean) stays one click
|
|
66
|
+
// away behind "Más opciones…".
|
|
67
|
+
const maggle = isMaggleMode();
|
|
68
|
+
let advancedVisible = !maggle;
|
|
69
|
+
if (maggle) {
|
|
70
|
+
SCHEMAS.plan.label = maggleText('launcher.planLabel', SCHEMAS.plan.label);
|
|
71
|
+
SCHEMAS.plan.help = maggleText('launcher.planHelp', SCHEMAS.plan.help);
|
|
72
|
+
SCHEMAS.plan.simpleFields = [
|
|
73
|
+
{ name: 'task', label: maggleText('launcher.taskLabel', 'Task'), type: 'textarea', rows: 6 },
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
let active = 'plan';
|
|
64
78
|
const renderForm = () => {
|
|
65
79
|
const s = SCHEMAS[active];
|
|
66
|
-
const
|
|
80
|
+
const visibleFields = (!advancedVisible && s.simpleFields) ? s.simpleFields : s.fields;
|
|
81
|
+
const fields = visibleFields.map((f) => {
|
|
67
82
|
const id = `cmd-field-${f.name}`;
|
|
68
83
|
if (f.type === 'textarea') {
|
|
69
84
|
return `
|
|
@@ -90,14 +105,14 @@ async function showCommandLauncher() {
|
|
|
90
105
|
|
|
91
106
|
dlg.innerHTML = `
|
|
92
107
|
<div style="padding:14px 18px;border-bottom:1px solid var(--border);font-weight:600;display:flex;align-items:center;justify-content:space-between;gap:10px">
|
|
93
|
-
<span
|
|
108
|
+
<span>${esc(maggleText('launcher.title', '⚡ Run a Karajan command'))}</span>
|
|
94
109
|
<button id="cmd-close" type="button" class="control-btn"
|
|
95
110
|
style="padding:4px 10px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text);border-radius:var(--radius-sm);cursor:pointer">
|
|
96
111
|
✕
|
|
97
112
|
</button>
|
|
98
113
|
</div>
|
|
99
114
|
<div style="padding:12px 18px;display:flex;gap:8px;border-bottom:1px solid var(--border)">
|
|
100
|
-
${Object.entries(SCHEMAS).map(([key, s]) => `
|
|
115
|
+
${Object.entries(SCHEMAS).filter(([key]) => advancedVisible || key === 'plan').map(([key, s]) => `
|
|
101
116
|
<button type="button" data-cmd="${key}"
|
|
102
117
|
style="padding:6px 12px;border:1px solid var(--border);
|
|
103
118
|
background:${key === active ? 'var(--color-green)' : 'var(--bg-primary)'};
|
|
@@ -106,6 +121,13 @@ async function showCommandLauncher() {
|
|
|
106
121
|
${esc(s.label)}
|
|
107
122
|
</button>
|
|
108
123
|
`).join('')}
|
|
124
|
+
${maggle && !advancedVisible ? `
|
|
125
|
+
<button type="button" id="cmd-more"
|
|
126
|
+
style="margin-left:auto;padding:6px 12px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text-muted);border-radius:var(--radius-sm);cursor:pointer;font-size:0.85rem"
|
|
127
|
+
title="Comandos técnicos: kj plan con ficheros, kj architect, kj clean">
|
|
128
|
+
${esc(maggleText('launcher.more', 'More…'))}
|
|
129
|
+
</button>
|
|
130
|
+
` : ''}
|
|
109
131
|
</div>
|
|
110
132
|
<form id="cmd-form" onsubmit="return false"
|
|
111
133
|
style="padding:14px 18px;display:flex;flex-direction:column;gap:12px;width:min(640px, 88vw);box-sizing:border-box">
|
|
@@ -114,11 +136,11 @@ async function showCommandLauncher() {
|
|
|
114
136
|
<div style="display:flex;justify-content:flex-end;gap:8px;padding-top:8px;border-top:1px solid var(--border)">
|
|
115
137
|
<button type="button" id="cmd-cancel" class="control-btn"
|
|
116
138
|
style="padding:6px 14px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text);border-radius:var(--radius-sm);cursor:pointer">
|
|
117
|
-
Cancel
|
|
139
|
+
${esc(maggleText('launcher.cancel', 'Cancel'))}
|
|
118
140
|
</button>
|
|
119
141
|
<button type="button" id="cmd-submit" class="control-btn"
|
|
120
142
|
style="padding:6px 14px;border:none;background:var(--color-green);color:#fff;border-radius:var(--radius-sm);cursor:pointer;font-weight:600">
|
|
121
|
-
Run
|
|
143
|
+
${esc(maggleText('launcher.submit', 'Run'))}
|
|
122
144
|
</button>
|
|
123
145
|
</div>
|
|
124
146
|
</form>
|
|
@@ -127,6 +149,8 @@ async function showCommandLauncher() {
|
|
|
127
149
|
dlg.querySelectorAll('[data-cmd]').forEach((btn) => {
|
|
128
150
|
btn.addEventListener('click', () => { active = btn.dataset.cmd; renderForm(); });
|
|
129
151
|
});
|
|
152
|
+
const moreBtn = dlg.querySelector('#cmd-more');
|
|
153
|
+
if (moreBtn) moreBtn.addEventListener('click', () => { advancedVisible = true; renderForm(); });
|
|
130
154
|
dlg.querySelector('#cmd-close').addEventListener('click', () => dlg.close());
|
|
131
155
|
dlg.querySelector('#cmd-cancel').addEventListener('click', () => dlg.close());
|
|
132
156
|
dlg.querySelector('#cmd-submit').addEventListener('click', () => submitCommand(active));
|
|
@@ -146,9 +170,21 @@ async function showCommandLauncher() {
|
|
|
146
170
|
}
|
|
147
171
|
// plan / architect: at least one of taskFile or task is required.
|
|
148
172
|
if ((command === 'plan' || command === 'architect') && !body.taskFile && !body.task) {
|
|
149
|
-
await showError(
|
|
173
|
+
await showError(
|
|
174
|
+
maggle ? 'Escribe primero qué quieres pedir — con tus palabras vale.' : 'Provide either a SPEC file path or paste the task text.',
|
|
175
|
+
{ title: 'Missing input' }
|
|
176
|
+
);
|
|
150
177
|
return;
|
|
151
178
|
}
|
|
179
|
+
// AC2: before launching, the maggle confirms knowing exactly what
|
|
180
|
+
// will happen — in their language, never a command name alone.
|
|
181
|
+
if (maggle && command === 'plan') {
|
|
182
|
+
const ok = await showConfirm(maggleText('launcher.confirm', ''), {
|
|
183
|
+
title: SCHEMAS.plan.label,
|
|
184
|
+
okLabel: maggleText('launcher.submit', 'Run'),
|
|
185
|
+
});
|
|
186
|
+
if (!ok) return;
|
|
187
|
+
}
|
|
152
188
|
if (command === 'clean' && body.nuke) {
|
|
153
189
|
const ok = await showConfirm(
|
|
154
190
|
'`kj clean --nuke` will delete EVERY plan / session / batch + wipe the HU Board DB. Cannot be undone.',
|
|
@@ -79,13 +79,13 @@ function openGenericLogPanel({ id, label, tailUrl }) {
|
|
|
79
79
|
border-bottom:1px solid var(--border);
|
|
80
80
|
user-select:none;flex-shrink:0">
|
|
81
81
|
<div style="display:flex;align-items:baseline;gap:10px;min-width:0">
|
|
82
|
-
<strong style="color:var(--text)">${esc(label)}</strong>
|
|
82
|
+
<strong style="color:var(--text)" title="${esc(label)}">${esc(maggleText('log.label', label))}</strong>
|
|
83
83
|
<span style="font-family:var(--font-mono, monospace);font-size:0.78rem;
|
|
84
84
|
color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;
|
|
85
85
|
white-space:nowrap">${esc(id)}</span>
|
|
86
86
|
</div>
|
|
87
87
|
<div style="display:flex;align-items:center;gap:6px;flex-shrink:0">
|
|
88
|
-
<span id="log-status" style="font-size:0.75rem;color:var(--text-muted);margin-right:8px"
|
|
88
|
+
<span id="log-status" style="font-size:0.75rem;color:var(--text-muted);margin-right:8px">${maggleText('log.connecting', 'connecting…')}</span>
|
|
89
89
|
<button id="log-min" type="button" title="Minimize (run keeps going)"
|
|
90
90
|
style="${winBtnStyle('#facc15')}">_</button>
|
|
91
91
|
<button id="log-max" type="button" title="Maximize / Restore"
|
|
@@ -101,8 +101,8 @@ function openGenericLogPanel({ id, label, tailUrl }) {
|
|
|
101
101
|
white-space:pre-wrap;word-break:break-word"></pre>
|
|
102
102
|
<footer style="padding:6px 12px;border-top:1px solid var(--border);
|
|
103
103
|
font-size:0.7rem;color:var(--text-muted);flex-shrink:0">
|
|
104
|
-
Closing this panel does NOT stop the run. It keeps going in the background;
|
|
105
|
-
reopen with the 📜 View log button on the board
|
|
104
|
+
${maggleText('log.footer', `Closing this panel does NOT stop the run. It keeps going in the background;
|
|
105
|
+
reopen with the 📜 View log button on the board.`)}
|
|
106
106
|
</footer>
|
|
107
107
|
`;
|
|
108
108
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// KJC-TSK-0810 (MGL-C) — maggle mode: the board in plain language.
|
|
2
|
+
//
|
|
3
|
+
// Classic script (no exports), loaded FIRST among the utils so the other
|
|
4
|
+
// scripts can call maggleText() at render time. `kj go` opens the board at
|
|
5
|
+
// /?maggle=1; the choice persists in localStorage so reloads keep the
|
|
6
|
+
// plain-language UI, and ?maggle=0 (the "modo experto" link) clears it.
|
|
7
|
+
// A dev opening the board directly sees no change. If storage is
|
|
8
|
+
// unavailable the mode degrades to the technical UI — it never breaks.
|
|
9
|
+
|
|
10
|
+
const MAGGLE_STORE_KEY = 'kj-maggle-mode';
|
|
11
|
+
|
|
12
|
+
// Plain label first; the technical term survives as tooltip/secondary
|
|
13
|
+
// detail where each call site renders it (AC1: llano delante, jerga detrás).
|
|
14
|
+
const MAGGLE_LABELS = {
|
|
15
|
+
'column.pending': 'Por hacer',
|
|
16
|
+
'column.running': 'En marcha',
|
|
17
|
+
'column.done': 'Hecho',
|
|
18
|
+
'board.title': 'Tu trabajo',
|
|
19
|
+
'board.stories': 'tareas',
|
|
20
|
+
'board.story': 'tarea',
|
|
21
|
+
'board.run': '▶ Continuar con lo pendiente',
|
|
22
|
+
'board.runTitle': 'Karajan seguirá con las tareas que faltan, una a una. Podrás ver la actividad aquí.',
|
|
23
|
+
'board.stop': '⏹ Parar',
|
|
24
|
+
'board.stopTitle': 'Detiene el trabajo en marcha. Lo ya terminado se conserva.',
|
|
25
|
+
'board.viewLog': '📜 Ver actividad',
|
|
26
|
+
'board.running': 'en marcha',
|
|
27
|
+
'board.ragPlaceholder': '🔍 Pregunta lo que quieras sobre este proyecto…',
|
|
28
|
+
'header.subtitle': 'tu tablero de trabajo',
|
|
29
|
+
'nav.board': 'Tablero',
|
|
30
|
+
'nav.dashboard': 'Proyectos',
|
|
31
|
+
'nav.more': 'Más',
|
|
32
|
+
'picker.hint': 'Elige un proyecto para ver su tablero. Puedes cambiar de proyecto arriba a la derecha.',
|
|
33
|
+
'picker.project': 'proyecto',
|
|
34
|
+
'launcher.title': '📝 Pedir trabajo a Karajan',
|
|
35
|
+
'launcher.planLabel': '📝 Pedir trabajo nuevo',
|
|
36
|
+
'launcher.planHelp': 'Cuéntale a Karajan qué quieres construir o cambiar, con tus palabras. Lo convertirá en tareas que verás en el tablero — todavía no toca tu código.',
|
|
37
|
+
'launcher.taskLabel': 'Qué quieres pedir',
|
|
38
|
+
'launcher.submit': 'Pedir',
|
|
39
|
+
'launcher.cancel': 'Cancelar',
|
|
40
|
+
'launcher.more': 'Más opciones…',
|
|
41
|
+
'launcher.confirm': 'Voy a convertir tu petición en tareas del tablero. Tardará unos minutos y podrás seguir la actividad en esta ventana. ¿Sigo?',
|
|
42
|
+
'conversation.title': 'Habla con tu agente',
|
|
43
|
+
'conversation.hide': 'Ocultar — la conversación sigue viva',
|
|
44
|
+
'conversation.end': 'Terminar la sesión del agente',
|
|
45
|
+
'log.label': 'Actividad',
|
|
46
|
+
'log.connecting': 'conectando…',
|
|
47
|
+
'log.footer': 'Cerrar esta ventana NO detiene el trabajo: sigue en marcha. Vuelve a abrirla con el botón «📜 Ver actividad» del tablero.',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function isMaggleMode() {
|
|
51
|
+
try {
|
|
52
|
+
const flag = new URLSearchParams(location.search || '').get('maggle');
|
|
53
|
+
if (flag === '1') { localStorage.setItem(MAGGLE_STORE_KEY, '1'); return true; }
|
|
54
|
+
if (flag === '0') { localStorage.removeItem(MAGGLE_STORE_KEY); return false; }
|
|
55
|
+
return localStorage.getItem(MAGGLE_STORE_KEY) === '1';
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Plain label when maggle mode is on; the technical one otherwise. */
|
|
62
|
+
function maggleText(key, technical) {
|
|
63
|
+
if (!isMaggleMode()) return technical;
|
|
64
|
+
return MAGGLE_LABELS[key] || technical;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// AC4: an error shown to a maggle is never a stacktrace alone. Pure —
|
|
68
|
+
// the caller (showError) decides by mode and renders the HTML.
|
|
69
|
+
function maggleErrorParts(rawMessage) {
|
|
70
|
+
return {
|
|
71
|
+
headline: 'Algo no ha salido como se esperaba',
|
|
72
|
+
next: 'No has roto nada. Vuelve a intentarlo; si se repite, cuéntaselo a tu agente en la conversación — este detalle le sirve para arreglarlo.',
|
|
73
|
+
detail: rawMessage,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// AC2 (nav half): the header keeps Tablero + Proyectos in plain Spanish
|
|
78
|
+
// and folds every advanced view behind one "Más" toggle, which also
|
|
79
|
+
// reveals the way back to the expert UI.
|
|
80
|
+
function applyMaggleChrome() {
|
|
81
|
+
if (!isMaggleMode()) return;
|
|
82
|
+
document.body.classList.add('maggle-mode');
|
|
83
|
+
const subtitle = document.querySelector('.header__subtitle');
|
|
84
|
+
if (subtitle) subtitle.textContent = MAGGLE_LABELS['header.subtitle'];
|
|
85
|
+
const nav = document.querySelector('.header__nav');
|
|
86
|
+
if (!nav) return;
|
|
87
|
+
const KEEP = { board: MAGGLE_LABELS['nav.board'], dashboard: MAGGLE_LABELS['nav.dashboard'] };
|
|
88
|
+
const advanced = [];
|
|
89
|
+
for (const btn of nav.querySelectorAll('.nav-btn')) {
|
|
90
|
+
const view = btn.dataset.view;
|
|
91
|
+
if (view && KEEP[view]) {
|
|
92
|
+
btn.title = `${btn.textContent.trim()} — ${btn.title}`;
|
|
93
|
+
btn.textContent = KEEP[view];
|
|
94
|
+
} else {
|
|
95
|
+
btn.style.display = 'none';
|
|
96
|
+
advanced.push(btn);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (advanced.length === 0) return;
|
|
100
|
+
const more = document.createElement('button');
|
|
101
|
+
more.className = 'nav-btn';
|
|
102
|
+
more.textContent = `${MAGGLE_LABELS['nav.more']} ▾`;
|
|
103
|
+
more.title = 'Vistas avanzadas y volver al modo experto';
|
|
104
|
+
more.addEventListener('click', () => {
|
|
105
|
+
const hidden = advanced[0].style.display === 'none';
|
|
106
|
+
for (const btn of advanced) btn.style.display = hidden ? '' : 'none';
|
|
107
|
+
more.textContent = `${MAGGLE_LABELS['nav.more']} ${hidden ? '▴' : '▾'}`;
|
|
108
|
+
let expert = document.getElementById('maggle-expert-link');
|
|
109
|
+
if (hidden && !expert) {
|
|
110
|
+
expert = document.createElement('a');
|
|
111
|
+
expert.id = 'maggle-expert-link';
|
|
112
|
+
expert.className = 'nav-btn';
|
|
113
|
+
expert.href = '/?maggle=0';
|
|
114
|
+
expert.textContent = 'Modo experto';
|
|
115
|
+
expert.title = 'Vuelve a la interfaz técnica (Pending/Running/Done, comandos kj)';
|
|
116
|
+
more.after(expert);
|
|
117
|
+
} else if (!hidden && expert) {
|
|
118
|
+
expert.remove();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// After the LAST nav button, so the plain row reads Tablero | Proyectos | Más.
|
|
122
|
+
advanced.at(-1).after(more);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (typeof document !== 'undefined' && document.addEventListener) {
|
|
126
|
+
document.addEventListener('DOMContentLoaded', applyMaggleChrome);
|
|
127
|
+
}
|
|
@@ -74,14 +74,23 @@ function ensureDialog() {
|
|
|
74
74
|
function showError(message, opts = {}) {
|
|
75
75
|
return new Promise((resolve) => {
|
|
76
76
|
const dlg = ensureDialog();
|
|
77
|
-
|
|
77
|
+
// Maggle mode (KJC-TSK-0810 AC4): plain headline + next step, the raw
|
|
78
|
+
// message demoted to a collapsed technical detail — never alone.
|
|
79
|
+
const maggle = isMaggleMode() ? maggleErrorParts(message) : null;
|
|
80
|
+
const title = maggle ? maggle.headline : (opts.title || 'Error');
|
|
81
|
+
const body = maggle
|
|
82
|
+
? `${esc(maggle.next)}
|
|
83
|
+
<details style="margin-top:10px"><summary style="cursor:pointer;color:var(--text-muted)">Detalle técnico${opts.title ? ` — ${esc(opts.title)}` : ''}</summary>
|
|
84
|
+
<pre style="white-space:pre-wrap;font-size:0.8rem;color:var(--text-muted);margin:8px 0 0">${esc(maggle.detail)}</pre>
|
|
85
|
+
</details>`
|
|
86
|
+
: esc(message);
|
|
78
87
|
dlg.innerHTML = `
|
|
79
88
|
<div style="padding:14px 18px;border-bottom:1px solid var(--border);
|
|
80
89
|
font-weight:600;color:var(--color-red,#ef4444)">
|
|
81
90
|
${esc(title)}
|
|
82
91
|
</div>
|
|
83
92
|
<div style="padding:16px 18px;font-size:0.9rem;line-height:1.5;
|
|
84
|
-
white-space:pre-wrap">${
|
|
93
|
+
white-space:pre-wrap">${body}</div>
|
|
85
94
|
<div style="padding:12px 18px;border-top:1px solid var(--border);
|
|
86
95
|
text-align:right">
|
|
87
96
|
<button id="app-dialog-ok" class="control-btn"
|
|
@@ -70,11 +70,11 @@ async function renderProjectPicker() {
|
|
|
70
70
|
|
|
71
71
|
app.innerHTML = `
|
|
72
72
|
<div class="section-header">
|
|
73
|
-
<span class="section-header__title"
|
|
74
|
-
<span class="section-header__count">${sorted.length} project${sorted.length === 1 ? '' : 's'}</span>
|
|
73
|
+
<span class="section-header__title">${maggleText('board.title', 'Story Board')}</span>
|
|
74
|
+
<span class="section-header__count">${sorted.length} ${maggleText('picker.project', 'project')}${sorted.length === 1 ? '' : 's'}</span>
|
|
75
75
|
</div>
|
|
76
76
|
<p style="padding:8px 4px 16px;color:var(--text-muted);font-size:0.9rem">
|
|
77
|
-
Pick a project to see its kanban. Use the project selector in the header to switch later.
|
|
77
|
+
${maggleText('picker.hint', 'Pick a project to see its kanban. Use the project selector in the header to switch later.')}
|
|
78
78
|
</p>
|
|
79
79
|
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(320px, 1fr));gap:14px">
|
|
80
80
|
${sorted.map((p) => {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// KJC-TSK-0816 (MGL-E, ADR 0008) — el panel «Conversación»: la terminal
|
|
2
|
+
// REAL del agente dentro del board. Script clásico, como el resto.
|
|
3
|
+
//
|
|
4
|
+
// Globals: Terminal (de /vendor/xterm), esc(), maggleText(), showError().
|
|
5
|
+
// El token del pty llega de /api/terminal/start (tras el auth del board)
|
|
6
|
+
// y viaja al WS como SUBPROTOCOLO, nunca en la URL.
|
|
7
|
+
|
|
8
|
+
let conversationState = { ws: null, term: null, panel: null, termId: null, token: null, opening: false };
|
|
9
|
+
|
|
10
|
+
async function openConversationPanel() {
|
|
11
|
+
if (conversationState.panel && conversationState.dead) {
|
|
12
|
+
// La conexión murió (agente terminado o red): reabrir = sesión fresca.
|
|
13
|
+
conversationState.panel.remove();
|
|
14
|
+
conversationState = { ws: null, term: null, panel: null, termId: null, token: null, opening: false };
|
|
15
|
+
}
|
|
16
|
+
if (conversationState.panel) {
|
|
17
|
+
conversationState.panel.style.display = 'flex';
|
|
18
|
+
conversationState.term?.focus();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Un doble clic (o ?window=1 + clic) no debe abrir dos sesiones.
|
|
22
|
+
if (conversationState.opening) return;
|
|
23
|
+
conversationState.opening = true;
|
|
24
|
+
let started;
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch('/api/terminal/start', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
// Sin agente en el body: decide el servidor (kj go --window fija el
|
|
30
|
+
// elegido por env; sin él, claude).
|
|
31
|
+
body: JSON.stringify({}),
|
|
32
|
+
});
|
|
33
|
+
started = await res.json();
|
|
34
|
+
if (!res.ok) throw new Error(started.error || `HTTP ${res.status}`);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
conversationState.opening = false;
|
|
37
|
+
await showError(err.message, { title: 'La conversación no pudo arrancar' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
conversationState.opening = false;
|
|
41
|
+
conversationState.termId = started.termId;
|
|
42
|
+
conversationState.token = started.token;
|
|
43
|
+
|
|
44
|
+
const panel = document.createElement('section');
|
|
45
|
+
panel.id = 'kj-conversation-panel';
|
|
46
|
+
panel.style.cssText = [
|
|
47
|
+
'position:fixed', 'top:0', 'right:0', 'bottom:0',
|
|
48
|
+
'width:min(720px, 55vw)', 'display:flex', 'flex-direction:column',
|
|
49
|
+
'background:#0b0b0c', 'border-left:1px solid var(--border)',
|
|
50
|
+
'box-shadow:-12px 0 40px rgba(0,0,0,0.45)', 'z-index:9998',
|
|
51
|
+
].join(';');
|
|
52
|
+
panel.innerHTML = `
|
|
53
|
+
<header style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;
|
|
54
|
+
background:var(--bg-primary);border-bottom:1px solid var(--border);flex-shrink:0">
|
|
55
|
+
<strong style="color:var(--text)">${esc(maggleText('conversation.title', 'Conversación con tu agente'))}</strong>
|
|
56
|
+
<div style="display:flex;gap:6px">
|
|
57
|
+
<button id="conv-hide" type="button" class="control-btn" title="${esc(maggleText('conversation.hide', 'Ocultar — la conversación sigue viva'))}"
|
|
58
|
+
style="padding:4px 10px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text);border-radius:var(--radius-sm);cursor:pointer">_</button>
|
|
59
|
+
<button id="conv-end" type="button" class="control-btn" title="${esc(maggleText('conversation.end', 'Terminar la sesión del agente'))}"
|
|
60
|
+
style="padding:4px 10px;border:1px solid var(--border);background:var(--bg-primary);color:#f87171;border-radius:var(--radius-sm);cursor:pointer">✕</button>
|
|
61
|
+
</div>
|
|
62
|
+
</header>
|
|
63
|
+
<div id="conv-term" style="flex:1 1 auto;min-height:0;padding:6px"></div>
|
|
64
|
+
`;
|
|
65
|
+
document.body.appendChild(panel);
|
|
66
|
+
conversationState.panel = panel;
|
|
67
|
+
|
|
68
|
+
const term = new Terminal({
|
|
69
|
+
convertEol: false,
|
|
70
|
+
cursorBlink: true,
|
|
71
|
+
fontFamily: 'JetBrains Mono, monospace',
|
|
72
|
+
fontSize: 13,
|
|
73
|
+
theme: { background: '#0b0b0c' },
|
|
74
|
+
});
|
|
75
|
+
term.open(panel.querySelector('#conv-term'));
|
|
76
|
+
conversationState.term = term;
|
|
77
|
+
|
|
78
|
+
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
79
|
+
const ws = new WebSocket(
|
|
80
|
+
`${proto}://${location.host}/api/terminal/ws?termId=${encodeURIComponent(started.termId)}`,
|
|
81
|
+
['kj-terminal', started.token],
|
|
82
|
+
);
|
|
83
|
+
conversationState.ws = ws;
|
|
84
|
+
ws.onmessage = (e) => term.write(typeof e.data === 'string' ? e.data : '');
|
|
85
|
+
ws.onclose = () => {
|
|
86
|
+
conversationState.dead = true;
|
|
87
|
+
term.write('\r\n[conexión cerrada — vuelve a abrir el panel para reconectar]\r\n');
|
|
88
|
+
};
|
|
89
|
+
term.onData((data) => {
|
|
90
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(data);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const sendResize = () => {
|
|
94
|
+
const el = panel.querySelector('#conv-term');
|
|
95
|
+
const cols = Math.max(40, Math.floor(el.clientWidth / 8));
|
|
96
|
+
const rows = Math.max(10, Math.floor(el.clientHeight / 18));
|
|
97
|
+
term.resize(cols, rows);
|
|
98
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
99
|
+
ws.send(`\u0000${JSON.stringify({ type: 'resize', cols, rows })}`);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
ws.onopen = () => { sendResize(); term.focus(); };
|
|
103
|
+
window.addEventListener('resize', sendResize);
|
|
104
|
+
|
|
105
|
+
panel.querySelector('#conv-hide').addEventListener('click', () => {
|
|
106
|
+
panel.style.display = 'none';
|
|
107
|
+
});
|
|
108
|
+
panel.querySelector('#conv-end').addEventListener('click', async () => {
|
|
109
|
+
try {
|
|
110
|
+
await fetch('/api/terminal/stop', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ termId: conversationState.termId, token: conversationState.token }),
|
|
114
|
+
});
|
|
115
|
+
} finally {
|
|
116
|
+
ws.close();
|
|
117
|
+
panel.remove();
|
|
118
|
+
conversationState = { ws: null, term: null, panel: null, termId: null, token: null, opening: false };
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// kj go --window abre el board con ?window=1: la conversación arranca sola.
|
|
124
|
+
if (typeof document !== 'undefined' && document.addEventListener) {
|
|
125
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
126
|
+
try {
|
|
127
|
+
if (new URLSearchParams(location.search).get('window') === '1') openConversationPanel();
|
|
128
|
+
} catch {
|
|
129
|
+
// Sin soporte de URLSearchParams no hay auto-apertura; el botón queda.
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|