janissary 0.5.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/LICENSE +75 -0
- package/README.md +28 -0
- package/agent-names.json +52 -0
- package/bin/janus.mjs +27 -0
- package/chrome-extension/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
- package/chrome-extension/content-script.js +62 -0
- package/chrome-extension/manifest.json +25 -0
- package/chrome-extension/rules.json +16 -0
- package/dist/acp/index.js +93 -0
- package/dist/acp/loop.js +75 -0
- package/dist/acp/manager.js +126 -0
- package/dist/acp/rate-limit.js +8 -0
- package/dist/acp/runner.js +18 -0
- package/dist/acp/tools.js +27 -0
- package/dist/acp-loop.js +75 -0
- package/dist/acp-manager.js +109 -0
- package/dist/acp-runner.js +18 -0
- package/dist/acp.js +91 -0
- package/dist/agent/commands.js +29 -0
- package/dist/agent/communication-manager.js +71 -0
- package/dist/agent/state.js +58 -0
- package/dist/agent-commands.js +29 -0
- package/dist/agent-communication-manager.js +71 -0
- package/dist/agent-state.js +58 -0
- package/dist/app-themes.js +13 -0
- package/dist/browser/command.js +104 -0
- package/dist/browser/index.js +124 -0
- package/dist/browser/tab-helpers.js +51 -0
- package/dist/browser/tab.js +99 -0
- package/dist/browser-command.js +104 -0
- package/dist/browser-tab-helpers.js +51 -0
- package/dist/browser-tab.js +99 -0
- package/dist/browser.js +124 -0
- package/dist/buffer.js +42 -0
- package/dist/bus.js +52 -0
- package/dist/capture/manager.js +48 -0
- package/dist/capture/router.js +18 -0
- package/dist/capture-manager.js +48 -0
- package/dist/capture-router.js +20 -0
- package/dist/chrome-extension-loader.js +62 -0
- package/dist/cli-args.js +80 -0
- package/dist/command-manager.js +126 -0
- package/dist/command-router.js +14 -0
- package/dist/commands/acp-reset.js +13 -0
- package/dist/commands/acp.js +5 -0
- package/dist/commands/agent.js +5 -0
- package/dist/commands/broadcast.js +22 -0
- package/dist/commands/browser.js +5 -0
- package/dist/commands/clear.js +5 -0
- package/dist/commands/close.js +42 -0
- package/dist/commands/connection.js +5 -0
- package/dist/commands/db.js +5 -0
- package/dist/commands/edit.js +18 -0
- package/dist/commands/files.js +13 -0
- package/dist/commands/hist.js +7 -0
- package/dist/commands/index.js +62 -0
- package/dist/commands/monitor.js +61 -0
- package/dist/commands/msg.js +17 -0
- package/dist/commands/next.js +7 -0
- package/dist/commands/notifications.js +16 -0
- package/dist/commands/notify.js +20 -0
- package/dist/commands/open.js +41 -0
- package/dist/commands/profile.js +5 -0
- package/dist/commands/queue.js +37 -0
- package/dist/commands/quit.js +6 -0
- package/dist/commands/rename.js +12 -0
- package/dist/commands/resolve-target.js +12 -0
- package/dist/commands/schedule.js +74 -0
- package/dist/commands/search.js +26 -0
- package/dist/commands/send.js +51 -0
- package/dist/commands/state.js +9 -0
- package/dist/commands/syntax.js +36 -0
- package/dist/commands/tasks.js +7 -0
- package/dist/commands/theme.js +41 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands.js +70 -0
- package/dist/completion/browser.js +24 -0
- package/dist/completion/fs.js +26 -0
- package/dist/completion/handlers.js +81 -0
- package/dist/completion/helpers.js +43 -0
- package/dist/completion/index.js +43 -0
- package/dist/completion-browser.js +24 -0
- package/dist/completion-fs.js +26 -0
- package/dist/completion-handlers.js +69 -0
- package/dist/completion-helpers.js +43 -0
- package/dist/completion.js +42 -0
- package/dist/config.js +66 -0
- package/dist/connection/close.js +41 -0
- package/dist/connection/manager.js +93 -0
- package/dist/connection/parsing.js +28 -0
- package/dist/connection-close.js +41 -0
- package/dist/connection-manager.js +93 -0
- package/dist/connection-parsing.js +28 -0
- package/dist/connections.js +75 -0
- package/dist/controller-events.js +34 -0
- package/dist/controller-file-tree.js +36 -0
- package/dist/controller.js +201 -0
- package/dist/database/index.js +83 -0
- package/dist/database/manager.js +67 -0
- package/dist/database/parsing.js +54 -0
- package/dist/database/query.js +41 -0
- package/dist/database-manager.js +67 -0
- package/dist/database-parsing.js +54 -0
- package/dist/database-query.js +41 -0
- package/dist/database.js +83 -0
- package/dist/datetime.js +15 -0
- package/dist/editor/save.js +27 -0
- package/dist/editor/sync.js +10 -0
- package/dist/editor/watch-manager.js +84 -0
- package/dist/editor-save.js +24 -0
- package/dist/editor-watch-manager.js +84 -0
- package/dist/file-tree-args.js +36 -0
- package/dist/file-tree-git-refresh.js +33 -0
- package/dist/file-tree-manager.js +252 -0
- package/dist/file-tree-moves.js +26 -0
- package/dist/file-tree-open.js +56 -0
- package/dist/file-tree-rebuild.js +22 -0
- package/dist/file-tree-watch.js +27 -0
- package/dist/file-tree.js +82 -0
- package/dist/git-status.js +64 -0
- package/dist/github-token.js +19 -0
- package/dist/global-history.js +48 -0
- package/dist/harness/auto-approve.js +79 -0
- package/dist/harness/busy-status.js +91 -0
- package/dist/harness/capture-file.js +28 -0
- package/dist/harness/index.js +87 -0
- package/dist/harness/manager.js +182 -0
- package/dist/harness/models.js +8 -0
- package/dist/harness/recorder.js +89 -0
- package/dist/harness/recording-file.js +25 -0
- package/dist/harness/screen.js +79 -0
- package/dist/harness-capture-file.js +28 -0
- package/dist/harness-manager.js +112 -0
- package/dist/harness-models.js +8 -0
- package/dist/harness-screen.js +72 -0
- package/dist/harness.js +54 -0
- package/dist/index.js +163 -0
- package/dist/instance-lock.js +34 -0
- package/dist/interactive.js +33 -0
- package/dist/main.js +203 -0
- package/dist/managers.js +1 -0
- package/dist/message-handler.js +134 -0
- package/dist/messaging.js +47 -0
- package/dist/monitor/acp.js +24 -0
- package/dist/monitor/ask.js +31 -0
- package/dist/monitor/context.js +27 -0
- package/dist/monitor/editor-feed.js +49 -0
- package/dist/monitor/feed-diff.js +22 -0
- package/dist/monitor/feeds.js +19 -0
- package/dist/monitor/framing.js +29 -0
- package/dist/monitor/harness-feed.js +21 -0
- package/dist/monitor/info.js +29 -0
- package/dist/monitor/manager.js +221 -0
- package/dist/monitor/page-feed.js +21 -0
- package/dist/monitor/parsing.js +82 -0
- package/dist/monitor/reply.js +11 -0
- package/dist/monitor/session.js +36 -0
- package/dist/monitor/stop.js +33 -0
- package/dist/monitor/suggestion.js +12 -0
- package/dist/monitor/targets.js +69 -0
- package/dist/monitor/window.js +109 -0
- package/dist/monitor-acp.js +22 -0
- package/dist/monitor-info.js +18 -0
- package/dist/monitor-manager.js +233 -0
- package/dist/monitor-parsing.js +68 -0
- package/dist/monitor-session.js +19 -0
- package/dist/monitor-targets.js +37 -0
- package/dist/monitor-window.js +81 -0
- package/dist/notifications-tab.js +32 -0
- package/dist/notifications.js +87 -0
- package/dist/open-file-manager.js +100 -0
- package/dist/openers/editor.js +41 -0
- package/dist/openers/external-viewer.js +11 -0
- package/dist/openers/image.js +27 -0
- package/dist/openers/index.js +16 -0
- package/dist/openers/markdown.js +20 -0
- package/dist/openers/os-open.js +24 -0
- package/dist/openers/page.js +51 -0
- package/dist/openers/size.js +13 -0
- package/dist/openers/types.js +1 -0
- package/dist/page/sync.js +10 -0
- package/dist/paths.js +45 -0
- package/dist/persona-parsing.js +49 -0
- package/dist/personas.js +44 -0
- package/dist/profile/agent-opener.js +154 -0
- package/dist/profile/files.js +17 -0
- package/dist/profile/manager.js +72 -0
- package/dist/profile/monitors.js +23 -0
- package/dist/profile/notifications.js +11 -0
- package/dist/profile-agent-opener.js +136 -0
- package/dist/profile-manager.js +72 -0
- package/dist/profiles.js +143 -0
- package/dist/protocol.js +1 -0
- package/dist/pseudoterminal-manager.js +106 -0
- package/dist/pty.js +41 -0
- package/dist/recognizers/acp.js +38 -0
- package/dist/recognizers/analyze.js +49 -0
- package/dist/recognizers/bash.js +51 -0
- package/dist/recognizers/db.js +26 -0
- package/dist/recognizers/index.js +1 -0
- package/dist/recognizers/lexicon.js +11 -0
- package/dist/recognizers/types.js +5 -0
- package/dist/resolve.js +39 -0
- package/dist/route-choice.js +11 -0
- package/dist/sandbox/index.js +217 -0
- package/dist/sandbox/paths.js +178 -0
- package/dist/sandbox/profile.js +161 -0
- package/dist/sandbox-paths.js +159 -0
- package/dist/sandbox-profile.js +161 -0
- package/dist/sandbox.js +217 -0
- package/dist/schedule/display.js +19 -0
- package/dist/schedule/helpers.js +76 -0
- package/dist/schedule/index.js +76 -0
- package/dist/schedule/manager.js +101 -0
- package/dist/schedule/parsing.js +63 -0
- package/dist/schedule/time.js +34 -0
- package/dist/schedule-display.js +19 -0
- package/dist/schedule-helpers.js +76 -0
- package/dist/schedule-manager.js +98 -0
- package/dist/schedule-parsing.js +63 -0
- package/dist/schedule-time.js +34 -0
- package/dist/schedule.js +76 -0
- package/dist/search-matches.js +40 -0
- package/dist/security.js +44 -0
- package/dist/shell-manager.js +139 -0
- package/dist/shell.js +59 -0
- package/dist/ssh-manager.js +42 -0
- package/dist/ssh.js +39 -0
- package/dist/startup-errors.js +21 -0
- package/dist/state-event.js +14 -0
- package/dist/state-format.js +38 -0
- package/dist/syntax-themes.js +14 -0
- package/dist/tab/agent-state.js +19 -0
- package/dist/tab/cleanup.js +33 -0
- package/dist/tab/colors.js +43 -0
- package/dist/tab/creators.js +95 -0
- package/dist/tab/dock.js +35 -0
- package/dist/tab/focus-history.js +41 -0
- package/dist/tab/formatting-handlers.js +77 -0
- package/dist/tab/formatting.js +21 -0
- package/dist/tab/history.js +14 -0
- package/dist/tab/index.js +65 -0
- package/dist/tab/manager.js +329 -0
- package/dist/tab/openers.js +35 -0
- package/dist/tab/queue.js +33 -0
- package/dist/tab/rehydrate.js +15 -0
- package/dist/tab/reorder.js +15 -0
- package/dist/tab/transcript.js +15 -0
- package/dist/tab/utils.js +55 -0
- package/dist/tab/view.js +42 -0
- package/dist/tab-cleanup.js +27 -0
- package/dist/tab-colors.js +43 -0
- package/dist/tab-creators.js +85 -0
- package/dist/tab-formatting-handlers.js +77 -0
- package/dist/tab-formatting.js +21 -0
- package/dist/tab-manager.js +418 -0
- package/dist/tab-utils.js +55 -0
- package/dist/tab.js +57 -0
- package/dist/tasks.js +32 -0
- package/dist/transcript/logger.js +36 -0
- package/dist/transcript/store.js +66 -0
- package/dist/types.js +6 -0
- package/dist/user-agent.js +65 -0
- package/dist/word-wrapping.js +42 -0
- package/dist/workspace-manager.js +40 -0
- package/dist/workspace.js +120 -0
- package/help.md +97 -0
- package/package.json +138 -0
package/dist/tab.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export { expandTabs, wordWrap, flattenBuffer } from './tab-formatting.js';
|
|
2
|
+
export { distinctColor, dotColors } from './tab-colors.js';
|
|
3
|
+
export { stripComments, renumberTabs, canMoveTab, swapTabsLeft, swapTabsRight, insertTabInGroup, uniqueLabel } from './tab-utils.js';
|
|
4
|
+
export const makeTab = (label, dotColor, number = 1, commandHistory = [], log = [], workspaceDirectory, group = 1, groupColor = dotColor) => ({
|
|
5
|
+
label,
|
|
6
|
+
dotColor,
|
|
7
|
+
number,
|
|
8
|
+
group,
|
|
9
|
+
groupColor,
|
|
10
|
+
log,
|
|
11
|
+
cmdHistory: commandHistory,
|
|
12
|
+
cmdHistoryIdx: -1,
|
|
13
|
+
scrollOffset: 0,
|
|
14
|
+
workspaceDir: workspaceDirectory,
|
|
15
|
+
});
|
|
16
|
+
// An image view tab (opened via `open <image>`). It carries no transcript/history/shell — just the
|
|
17
|
+
// image payload — and is always titled `image` while keeping a unique `label` so several can coexist.
|
|
18
|
+
export const makeImageTab = (label, dotColor, number, group, groupColor, image) => ({
|
|
19
|
+
...makeTab(label, dotColor, number, [], [], undefined, group, groupColor),
|
|
20
|
+
view: 'image',
|
|
21
|
+
title: 'image',
|
|
22
|
+
image,
|
|
23
|
+
});
|
|
24
|
+
// A page view tab (opened via `open https://…` or `open page …`). Renders an iframe; carries no
|
|
25
|
+
// transcript/history/shell. The title shows the domain (e.g. "slashdot.org").
|
|
26
|
+
export const makePageTab = (label, dotColor, number, group, groupColor, page) => ({
|
|
27
|
+
...makeTab(label, dotColor, number, [], [], undefined, group, groupColor),
|
|
28
|
+
view: 'page',
|
|
29
|
+
title: page.domain,
|
|
30
|
+
page,
|
|
31
|
+
});
|
|
32
|
+
// A markdown view tab (opened via `open <file>.md`). Renders the file as formatted Markdown.
|
|
33
|
+
export const makeMarkdownTab = (label, dotColor, number, group, groupColor, markdown) => ({
|
|
34
|
+
...makeTab(label, dotColor, number, [], [], undefined, group, groupColor),
|
|
35
|
+
view: 'markdown',
|
|
36
|
+
title: 'markdown',
|
|
37
|
+
markdown,
|
|
38
|
+
});
|
|
39
|
+
// An editor view tab (opened via `open <text file>` or `edit <file>`). Hosts the plain-text editor.
|
|
40
|
+
export const makeEditorTab = (label, dotColor, number, group, groupColor, editor) => ({
|
|
41
|
+
...makeTab(label, dotColor, number, [], [], undefined, group, groupColor),
|
|
42
|
+
view: 'editor',
|
|
43
|
+
title: editor.name,
|
|
44
|
+
editor,
|
|
45
|
+
});
|
|
46
|
+
// A harness view tab (opened via `harness <name>`). The entire tab body is a live PTY terminal.
|
|
47
|
+
export const makeHarnessTab = (label, dotColor, number, group, groupColor, harness, workspaceDirectory) => ({
|
|
48
|
+
...makeTab(label, dotColor, number, [], [], workspaceDirectory, group, groupColor),
|
|
49
|
+
view: 'harness', title: label, harness,
|
|
50
|
+
});
|
|
51
|
+
// A file tree view tab (opened via `files [path]`). Shows a directory tree rooted at `files.root`.
|
|
52
|
+
export const makeFilesTab = (label, dotColor, number, group, groupColor, files) => ({
|
|
53
|
+
...makeTab(label, dotColor, number, [], [], undefined, group, groupColor),
|
|
54
|
+
view: 'files',
|
|
55
|
+
title: 'navigator',
|
|
56
|
+
files,
|
|
57
|
+
});
|
package/dist/tasks.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
// Depth-first walk of one directory: returns its entries (files and subdirectories, recursed
|
|
4
|
+
// into) as a flat, pre-order `TaskRow[]` — a directory row immediately followed by its children,
|
|
5
|
+
// so the array is already in display order. Sorted alphabetically within each directory.
|
|
6
|
+
function walk(dir, relativeDir, depth) {
|
|
7
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
8
|
+
.filter((dirent) => dirent.isDirectory() || (dirent.isFile() && dirent.name.endsWith('.md')))
|
|
9
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
10
|
+
const rows = [];
|
|
11
|
+
for (const dirent of entries) {
|
|
12
|
+
const relPath = relativeDir ? `${relativeDir}/${dirent.name}` : dirent.name;
|
|
13
|
+
if (dirent.isDirectory()) {
|
|
14
|
+
rows.push({ path: relPath, name: dirent.name, depth, dir: true }, ...walk(path.join(dir, dirent.name), relPath, depth + 1));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
rows.push({ path: relPath, name: dirent.name, depth, dir: false });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return rows;
|
|
21
|
+
}
|
|
22
|
+
// Executable task prompts: markdown files under `ai/tasks/` (`build-a-feature.md`,
|
|
23
|
+
// `fix-a-small-issue.md`, …), recursed into any subdirectory. The `.md` extension is kept in
|
|
24
|
+
// `path`: the picker inserts `execute ./ai/tasks/<path>` verbatim.
|
|
25
|
+
export function listTasks(root = process.cwd()) {
|
|
26
|
+
try {
|
|
27
|
+
return walk(path.join(root, 'ai', 'tasks'), '', 0);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdirSync, appendFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getDateStr, getTimeStr } from '../datetime.js';
|
|
4
|
+
import { messageBus } from '../bus.js';
|
|
5
|
+
let logDir = '';
|
|
6
|
+
export class TranscriptLogger {
|
|
7
|
+
static get logDir() {
|
|
8
|
+
return logDir;
|
|
9
|
+
}
|
|
10
|
+
static append(entry) {
|
|
11
|
+
if (!logDir)
|
|
12
|
+
return;
|
|
13
|
+
const logPath = path.join(logDir, `${getDateStr()}.json`);
|
|
14
|
+
appendFileSync(logPath, JSON.stringify(entry) + '\n');
|
|
15
|
+
}
|
|
16
|
+
sub;
|
|
17
|
+
constructor(projectDir) {
|
|
18
|
+
if (projectDir) {
|
|
19
|
+
logDir = path.join(projectDir, '.janissary', 'log');
|
|
20
|
+
mkdirSync(logDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
this.sub = messageBus.on('transcript', 'entry:appended', (event) => {
|
|
23
|
+
if (event.type !== 'entry:appended')
|
|
24
|
+
return;
|
|
25
|
+
const { tabLabel, entry } = event;
|
|
26
|
+
const ts = getTimeStr();
|
|
27
|
+
if (entry.input)
|
|
28
|
+
TranscriptLogger.append({ timestamp: ts, agent: tabLabel, text: entry.input });
|
|
29
|
+
if (entry.output)
|
|
30
|
+
TranscriptLogger.append({ timestamp: ts, agent: tabLabel, text: entry.output });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
unsubscribe() {
|
|
34
|
+
this.sub.unsubscribe();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { messageBus } from '../bus.js';
|
|
4
|
+
const VALID_NAME = /^[\w-]+$/;
|
|
5
|
+
let transcriptDir = '';
|
|
6
|
+
// Subscribes to the bus and persists transcript files whenever entries are appended or a tab is
|
|
7
|
+
// cleared. tab:removed keeps the file for history. In-place edits (streaming, runShell completion)
|
|
8
|
+
// are persisted by direct calls to save() from the controller since there is no bus event for them.
|
|
9
|
+
export class TranscriptStore {
|
|
10
|
+
static path(label) {
|
|
11
|
+
if (!VALID_NAME.test(label))
|
|
12
|
+
throw new Error(`Invalid transcript label: "${label}"`);
|
|
13
|
+
return path.join(transcriptDir, `${label}.json`);
|
|
14
|
+
}
|
|
15
|
+
static load(label) {
|
|
16
|
+
if (!transcriptDir)
|
|
17
|
+
return undefined;
|
|
18
|
+
const p = this.path(label);
|
|
19
|
+
if (!existsSync(p))
|
|
20
|
+
return undefined;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
static save(label, log) {
|
|
29
|
+
if (!transcriptDir)
|
|
30
|
+
return;
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(transcriptDir, { recursive: true });
|
|
33
|
+
writeFileSync(this.path(label), JSON.stringify(log));
|
|
34
|
+
}
|
|
35
|
+
catch { /* ignore */ }
|
|
36
|
+
}
|
|
37
|
+
static clearTab(label) {
|
|
38
|
+
if (!transcriptDir)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
mkdirSync(transcriptDir, { recursive: true });
|
|
42
|
+
writeFileSync(this.path(label), '[]');
|
|
43
|
+
}
|
|
44
|
+
catch { /* ignore */ }
|
|
45
|
+
}
|
|
46
|
+
static clear() {
|
|
47
|
+
if (!transcriptDir)
|
|
48
|
+
return;
|
|
49
|
+
try {
|
|
50
|
+
rmSync(transcriptDir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
catch { /* ignore */ }
|
|
53
|
+
}
|
|
54
|
+
constructor(projectDir) {
|
|
55
|
+
if (projectDir)
|
|
56
|
+
transcriptDir = path.join(projectDir, '.janissary', 'transcripts');
|
|
57
|
+
messageBus.on('transcript', 'entry:appended', (event) => {
|
|
58
|
+
if (event.type === 'entry:appended')
|
|
59
|
+
TranscriptStore.save(event.tabLabel, event.tab.log);
|
|
60
|
+
});
|
|
61
|
+
messageBus.on('transcript', 'tab:cleared', (event) => {
|
|
62
|
+
if (event.type === 'tab:cleared')
|
|
63
|
+
TranscriptStore.clearTab(event.tabLabel);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
// Shared type declarations for the top-level `src/` modules. Types live here rather than
|
|
3
|
+
// alongside their implementations so each directory has a single types file (the
|
|
4
|
+
// `src/commands/` directory has its own `commands/types.ts`). Runtime values (functions,
|
|
5
|
+
// constants) stay in their respective modules; only types are collected here.
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// A randomized-but-coherent browser fingerprint per window (BrowserContext). The point is
|
|
2
|
+
// twofold: (1) isolated windows shouldn't share an identical fingerprint, and (2) every
|
|
3
|
+
// signal we expose must be internally consistent — a user agent that disagrees with the
|
|
4
|
+
// platform, locale, timezone, or client-hint headers is itself a bot tell. So a profile
|
|
5
|
+
// bundles the UA string with the matching `Sec-CH-UA-Platform` value, an Accept-Language
|
|
6
|
+
// header, a timezone plausible for the platform, and a common desktop viewport.
|
|
7
|
+
//
|
|
8
|
+
// The Chrome *version* is pinned to the real Chromium build at the call site (passed in as
|
|
9
|
+
// `chromeMajor`) so the UA string and the engine-emitted client hints report the same
|
|
10
|
+
// version. Only the platform/locale/timezone/viewport are randomized.
|
|
11
|
+
const PLATFORMS = [
|
|
12
|
+
{
|
|
13
|
+
token: 'Windows NT 10.0; Win64; x64',
|
|
14
|
+
platform: 'Windows',
|
|
15
|
+
timezones: ['America/New_York', 'America/Chicago', 'America/Los_Angeles', 'Europe/London'],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
token: 'Macintosh; Intel Mac OS X 10_15_7',
|
|
19
|
+
platform: 'macOS',
|
|
20
|
+
timezones: ['America/New_York', 'America/Los_Angeles', 'Europe/London'],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
token: 'X11; Linux x86_64',
|
|
24
|
+
platform: 'Linux',
|
|
25
|
+
timezones: ['Europe/Berlin', 'Europe/Paris', 'America/New_York'],
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
// Common desktop resolutions; nothing exotic that would stand out.
|
|
29
|
+
const VIEWPORTS = [
|
|
30
|
+
{ width: 1920, height: 1080 },
|
|
31
|
+
{ width: 1536, height: 864 },
|
|
32
|
+
{ width: 1440, height: 900 },
|
|
33
|
+
{ width: 1366, height: 768 },
|
|
34
|
+
{ width: 1280, height: 720 },
|
|
35
|
+
];
|
|
36
|
+
const LOCALES = ['en-US', 'en-GB'];
|
|
37
|
+
// Fallback when the real Chromium version can't be read; kept recent.
|
|
38
|
+
export const DEFAULT_CHROME_MAJOR = 149;
|
|
39
|
+
function pick(array, rand) {
|
|
40
|
+
return array[Math.floor(rand() * array.length)];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Generate a coherent browser profile. `chromeMajor` should be the real engine's major
|
|
44
|
+
* version so the UA matches the client hints; `rand` is injectable for deterministic tests.
|
|
45
|
+
*/
|
|
46
|
+
export function randomBrowserProfile(chromeMajor = DEFAULT_CHROME_MAJOR, rand = Math.random) {
|
|
47
|
+
const p = pick(PLATFORMS, rand);
|
|
48
|
+
const userAgent = `Mozilla/5.0 (${p.token}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeMajor}.0.0.0 Safari/537.36`;
|
|
49
|
+
return {
|
|
50
|
+
userAgent,
|
|
51
|
+
platform: p.platform,
|
|
52
|
+
locale: pick(LOCALES, rand),
|
|
53
|
+
timezoneId: pick(p.timezones, rand),
|
|
54
|
+
viewport: pick(VIEWPORTS, rand),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Just the UA string from a fresh profile (convenience for callers that only need that). */
|
|
58
|
+
export function randomUserAgent(chromeMajor = DEFAULT_CHROME_MAJOR, rand = Math.random) {
|
|
59
|
+
return randomBrowserProfile(chromeMajor, rand).userAgent;
|
|
60
|
+
}
|
|
61
|
+
/** Build an `Accept-Language` header value consistent with a profile's locale. */
|
|
62
|
+
export function acceptLanguage(locale) {
|
|
63
|
+
const base = locale.split('-', 1)[0];
|
|
64
|
+
return locale === base ? `${locale};q=0.9` : `${locale},${base};q=0.9`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function wrapLine(line, width) {
|
|
2
|
+
const out = [];
|
|
3
|
+
let current = '';
|
|
4
|
+
for (const word of line.split(' ')) {
|
|
5
|
+
if (word.length > width) {
|
|
6
|
+
if (current)
|
|
7
|
+
out.push(current);
|
|
8
|
+
let w = word;
|
|
9
|
+
while (w.length > width) {
|
|
10
|
+
out.push(w.slice(0, width));
|
|
11
|
+
w = w.slice(width);
|
|
12
|
+
}
|
|
13
|
+
current = w;
|
|
14
|
+
}
|
|
15
|
+
else if (current === '') {
|
|
16
|
+
current = word;
|
|
17
|
+
}
|
|
18
|
+
else if (current.length + 1 + word.length <= width) {
|
|
19
|
+
current += ' ' + word;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
out.push(current);
|
|
23
|
+
current = word;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (current)
|
|
27
|
+
out.push(current);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
export function wordWrap(text, width) {
|
|
31
|
+
if (width <= 0)
|
|
32
|
+
return text;
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const line of text.split('\n')) {
|
|
35
|
+
if (line.length <= width) {
|
|
36
|
+
out.push(line);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
out.push(...wrapLine(line, width));
|
|
40
|
+
}
|
|
41
|
+
return out.join('\n');
|
|
42
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { findRepoRoot, createWorkspace, removeWorkspace } from './workspace.js';
|
|
2
|
+
const NO_REPO = 'No git repository found. Cannot create workspace.';
|
|
3
|
+
// Owns the set of workspace clones the app has created — an independent `git clone` of the repo's
|
|
4
|
+
// `origin` remote, made for an agent (`agent --workspace`) or a harness tab (`harness <name>
|
|
5
|
+
// --workspace`) so it works in isolation. Tracks each clone so it can be removed when its tab
|
|
6
|
+
// closes or at shutdown.
|
|
7
|
+
export class WorkspaceManager {
|
|
8
|
+
dirs = new Set();
|
|
9
|
+
projectDir;
|
|
10
|
+
constructor(projectDir) {
|
|
11
|
+
this.projectDir = projectDir ?? process.cwd();
|
|
12
|
+
}
|
|
13
|
+
// Create a workspace clone named `name` from the repo detected at the launch cwd, tracking it for
|
|
14
|
+
// cleanup. Returns the new directory, or an `{ error }` to surface when there's no repo / the clone
|
|
15
|
+
// fails. Shared by the agent and harness `--workspace` paths so both behave identically.
|
|
16
|
+
create(name) {
|
|
17
|
+
const root = findRepoRoot(this.projectDir);
|
|
18
|
+
if (!root)
|
|
19
|
+
return { error: NO_REPO };
|
|
20
|
+
try {
|
|
21
|
+
const dir = createWorkspace(name, root);
|
|
22
|
+
this.dirs.add(dir);
|
|
23
|
+
return { dir };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return { error: `Failed to create workspace: ${error instanceof Error ? error.message : String(error)}` };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Remove a workspace clone and stop tracking it (on tab close).
|
|
30
|
+
remove(dir) {
|
|
31
|
+
removeWorkspace(dir);
|
|
32
|
+
this.dirs.delete(dir);
|
|
33
|
+
}
|
|
34
|
+
// Remove every workspace clone (app shutdown).
|
|
35
|
+
removeAll() {
|
|
36
|
+
for (const dir of this.dirs)
|
|
37
|
+
removeWorkspace(dir);
|
|
38
|
+
this.dirs.clear();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
let workspaceBaseDir = '';
|
|
6
|
+
export function initWorkspaceDir(projectDir) {
|
|
7
|
+
workspaceBaseDir = path.join(projectDir, '.janissary', 'workspace');
|
|
8
|
+
}
|
|
9
|
+
export function ensureWorkspaceDir() {
|
|
10
|
+
mkdirSync(workspaceBaseDir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
export function workspacePath(name) {
|
|
13
|
+
if (!workspaceBaseDir)
|
|
14
|
+
throw new Error('Workspace dir not initialized. Call initWorkspaceDir first.');
|
|
15
|
+
return path.join(workspaceBaseDir, name);
|
|
16
|
+
}
|
|
17
|
+
export function findRepoRoot(from) {
|
|
18
|
+
let directory = from;
|
|
19
|
+
while (true) {
|
|
20
|
+
if (existsSync(path.join(directory, '.git')))
|
|
21
|
+
return directory;
|
|
22
|
+
const parent = path.dirname(directory);
|
|
23
|
+
if (parent === directory)
|
|
24
|
+
return undefined;
|
|
25
|
+
directory = parent;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function trustWorkspace(workspaceDir) {
|
|
29
|
+
const claudeJson = path.join(homedir(), '.claude.json');
|
|
30
|
+
let data = {};
|
|
31
|
+
try {
|
|
32
|
+
data = JSON.parse(readFileSync(claudeJson, 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
catch { /* file absent or unparseable — start fresh */ }
|
|
35
|
+
const projects = (data['projects'] ?? {});
|
|
36
|
+
projects[workspaceDir] = { ...projects[workspaceDir], hasTrustDialogAccepted: true };
|
|
37
|
+
data['projects'] = projects;
|
|
38
|
+
writeFileSync(claudeJson, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
39
|
+
}
|
|
40
|
+
// The workspace's private scratch dir, a sibling of the clone (`<name>.tmp`) — exported as
|
|
41
|
+
// `TMPDIR` for a sandboxed workspace so scratch writes don't need to share global `/tmp` across
|
|
42
|
+
// agents (see `sandbox.ts`).
|
|
43
|
+
export function workspaceTempPath(name) {
|
|
44
|
+
return `${workspacePath(name)}.tmp`;
|
|
45
|
+
}
|
|
46
|
+
export function getRemoteUrl(repoPath) {
|
|
47
|
+
// Intentional: user-driven workspace creation; only local-user commands reach this sink.
|
|
48
|
+
const url = execSync('git remote get-url origin', { cwd: repoPath, stdio: 'pipe' }).toString().trim();
|
|
49
|
+
if (!url)
|
|
50
|
+
throw new Error(`No "origin" remote configured for ${repoPath}`);
|
|
51
|
+
return url;
|
|
52
|
+
}
|
|
53
|
+
// Handles `git@github.com:owner/repo.git` and `ssh://git@github.com/owner/repo.git`; an
|
|
54
|
+
// already-HTTPS URL passes through unchanged.
|
|
55
|
+
export function toHttpsUrl(url) {
|
|
56
|
+
const scpMatch = /^git@([^:]+):(.+?)(\.git)?$/.exec(url);
|
|
57
|
+
if (scpMatch)
|
|
58
|
+
return `https://${scpMatch[1]}/${scpMatch[2]}.git`;
|
|
59
|
+
const sshMatch = /^ssh:\/\/git@([^/]+)\/(.+?)(\.git)?$/.exec(url);
|
|
60
|
+
if (sshMatch)
|
|
61
|
+
return `https://${sshMatch[1]}/${sshMatch[2]}.git`;
|
|
62
|
+
return url;
|
|
63
|
+
}
|
|
64
|
+
export function createWorkspace(name, repoPath) {
|
|
65
|
+
ensureWorkspaceDir();
|
|
66
|
+
const target = workspacePath(name);
|
|
67
|
+
const remoteUrl = getRemoteUrl(repoPath);
|
|
68
|
+
// Clone over whatever transport already works on the host (this runs unsandboxed, so SSH is
|
|
69
|
+
// fine here) — intentional: user-driven workspace creation; only local-user commands reach this
|
|
70
|
+
// sink.
|
|
71
|
+
execSync(`git clone "${remoteUrl}" "${target}"`, { stdio: 'pipe' });
|
|
72
|
+
// Rewrite the clone's own origin to HTTPS: later git operations from *inside* the workspace run
|
|
73
|
+
// in the Seatbelt sandbox, which denies `~/.ssh` and scrubs `SSH_AUTH_SOCK`, so SSH can't
|
|
74
|
+
// authenticate there — only HTTPS + the injected `GH_TOKEN` can (see sandbox.ts).
|
|
75
|
+
execSync(`git remote set-url origin "${toHttpsUrl(remoteUrl)}"`, { cwd: target, stdio: 'pipe' });
|
|
76
|
+
// Local-only credential helper (never touches global git config) — `gh auth git-credential`
|
|
77
|
+
// checks `GH_TOKEN` in its environment before falling back to its keychain-stored OAuth token,
|
|
78
|
+
// so once the sandbox injects `GH_TOKEN` (see sandbox.ts), `git push` authenticates via it.
|
|
79
|
+
execSync('git config --local credential.helper "!gh auth git-credential"', { cwd: target, stdio: 'pipe' });
|
|
80
|
+
trustWorkspace(target);
|
|
81
|
+
mkdirSync(workspaceTempPath(name), { recursive: true });
|
|
82
|
+
return target;
|
|
83
|
+
}
|
|
84
|
+
export function untrustWorkspace(workspaceDir) {
|
|
85
|
+
const claudeJson = path.join(homedir(), '.claude.json');
|
|
86
|
+
let data;
|
|
87
|
+
try {
|
|
88
|
+
data = JSON.parse(readFileSync(claudeJson, 'utf8'));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const projects = data['projects'];
|
|
94
|
+
if (!projects || !Object.hasOwn(projects, workspaceDir))
|
|
95
|
+
return;
|
|
96
|
+
delete projects[workspaceDir];
|
|
97
|
+
writeFileSync(claudeJson, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
98
|
+
}
|
|
99
|
+
export function removeWorkspace(directory) {
|
|
100
|
+
untrustWorkspace(directory);
|
|
101
|
+
try {
|
|
102
|
+
rmSync(directory, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
catch { /* ignore */ }
|
|
105
|
+
try {
|
|
106
|
+
rmSync(`${directory}.tmp`, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
catch { /* ignore */ }
|
|
109
|
+
}
|
|
110
|
+
export function clearWorkspaceDir() {
|
|
111
|
+
if (!workspaceBaseDir)
|
|
112
|
+
return;
|
|
113
|
+
try {
|
|
114
|
+
const entries = readdirSync(workspaceBaseDir);
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
rmSync(path.join(workspaceBaseDir, entry), { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch { /* ignore */ }
|
|
120
|
+
}
|
package/help.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
### Commands
|
|
2
|
+
|
|
3
|
+
| Command | Description |
|
|
4
|
+
| ------- | ----------- |
|
|
5
|
+
| `help` | List available commands |
|
|
6
|
+
| `state` | Show agent state fields (truncated) |
|
|
7
|
+
| `clear` | Clear the output log |
|
|
8
|
+
| `quit` | Exit the application (asks for confirmation) |
|
|
9
|
+
| `close` | Close the current tab (exits if last); `close page #` closes a numbered page tab; `close <tabname>` closes a tab by its label. `exit` is an alias |
|
|
10
|
+
| `agent` | Create a new agent tab (add `--workspace` / `-w` to clone the repo, isolated by default — see Workspace; add `--offline` to also deny network access) |
|
|
11
|
+
| `next` | Switch to the next tab |
|
|
12
|
+
| `hist` | Open command history picker |
|
|
13
|
+
| `tasks` | Open the task picker listing executable `ai/*.md` task files (Ctrl+A) |
|
|
14
|
+
| `nav` | Open the fuzzy tab navigator (Ctrl+G); `nav <query>` pre-fills the search |
|
|
15
|
+
| `msg` | Send a message to another agent |
|
|
16
|
+
| `broadcast` | Send a message to several or all agents |
|
|
17
|
+
| `acp` | Send a prompt to the OpenCode ACP agent |
|
|
18
|
+
| `db` | Create, delete, query, or list SQLite databases |
|
|
19
|
+
| `browser` | Drive a headless/headed web browser (open, goto, content, eval, shot) |
|
|
20
|
+
| `open` | Open images/files in a tab, or web pages embedded (`open https://…` / `open page …`) — sites that refuse framing render too; `open external` uses the OS viewer/browser |
|
|
21
|
+
| `edit` | Open a file in the plain-text editor (`edit <file>` or `edit <file>:<line>` to jump to a line) |
|
|
22
|
+
| `rename` | Rename the current tab's display name (`rename <name>`); bare `rename` clears the alias |
|
|
23
|
+
| `connection` | List or close open connections (sqlite/shell/acp/browser) |
|
|
24
|
+
| `schedule` | Run a command later — once or on a recurring schedule |
|
|
25
|
+
| `profile` | Launch a saved set of agents for a use case |
|
|
26
|
+
| `harness` | Open an AI coding harness (claude/opencode/codex) in a full-tab terminal (add `-w` / `--workspace` to clone the repo, `-y` / `--yes` to auto-approve claude's permission prompts — claude only, requires `-w`); `harness capture <name>` snapshots a harness tab's screen into an editor tab |
|
|
27
|
+
| `ssh` | Open an SSH session to a remote host in a full-tab terminal |
|
|
28
|
+
| `search` | `search transcript <pattern>` searches the current tab's transcript with a regex (Cmd+F opens it empty) |
|
|
29
|
+
| `files` | `files [path]` opens a file tree tab rooted at the issuing tab's cwd, or at `path` |
|
|
30
|
+
| `notifications` | `notifications [left\|right]` opens (or docks) the notifications tab — a feed of background-tab events (see `.janissary/config.json` to enable events) |
|
|
31
|
+
| `notify` | `notify <message>` pushes a custom line into the notifications feed (dropped if the tab is closed) |
|
|
32
|
+
| `send` | Deliver a line of input to any tab — types into a harness, or runs a command in an agent tab |
|
|
33
|
+
| `queue` | Queue a command for another agent tab (`queue <agent> <command>`); bare `queue` opens the interactive queue picker (Ctrl+E) |
|
|
34
|
+
| `monitor` | Start a persona-driven AI monitor — inline on the current tab, or watching other tabs/groups into a reporting tab |
|
|
35
|
+
| `unmonitor` | Stop a monitor (`unmonitor <persona>`) or all monitors started from this tab (`--all`) |
|
|
36
|
+
| `monitors` | List active monitors with their targets and suggestion counts |
|
|
37
|
+
| `theme` | Set the application UI theme (`theme <name>`); `theme` alone lists available themes; `theme sync` sets the syntax theme to match the app theme name |
|
|
38
|
+
| `syntax` | `syntax theme <name>` sets the editor tab's syntax-highlighting theme (applies to every open editor tab); `syntax theme` alone opens a theme-picker modal |
|
|
39
|
+
|
|
40
|
+
### Key Bindings
|
|
41
|
+
|
|
42
|
+
| Key | Action |
|
|
43
|
+
| --- | ------ |
|
|
44
|
+
| `←` / `→` | Move cursor in the input field |
|
|
45
|
+
| `↑` / `↓` | Previous / next command in history |
|
|
46
|
+
| `Shift+←` / `Shift+→` / `Cmd+Shift+[` / `Cmd+Shift+]` | Switch to the previous / next tab |
|
|
47
|
+
| `Ctrl+←` / `Ctrl+→` | Move the current tab left / right |
|
|
48
|
+
| `Shift+↑` / `Shift+↓` | Scroll the transcript up / down (accelerated — distance doubles each second) |
|
|
49
|
+
| `Ctrl+↑` / `Ctrl+↓` | Scroll the transcript up / down (accelerated) |
|
|
50
|
+
| `Page Up` / `Page Down` | Scroll the transcript up / down by half terminal height |
|
|
51
|
+
| `Escape` | Reset scroll to bottom |
|
|
52
|
+
| `Ctrl+P` / `Ctrl+N` | Scroll the transcript up / down one line (fixed) |
|
|
53
|
+
| `Ctrl+R` | Open command history picker |
|
|
54
|
+
| `Ctrl+G` | Open the fuzzy tab navigator (also closes it if already open) |
|
|
55
|
+
| `Ctrl+E` | Open the queue picker to send a command to another agent tab |
|
|
56
|
+
| `Ctrl+A` | Open the task picker (executable `ai/*.md` files); Return populates the command line without running |
|
|
57
|
+
| `Ctrl+T` | Expand / collapse agent tool steps in the transcript |
|
|
58
|
+
| `Cmd+T` | Open a new agent tab (same as typing `agent`) |
|
|
59
|
+
| `Cmd+F` | Open the search bar in the transcript |
|
|
60
|
+
| `Cmd+W` / `Ctrl+W` | Close the current tab |
|
|
61
|
+
| `Tab` | Complete a file path, an agent name for `msg` / `broadcast`, a connection string for `connection close`, a `browser` subcommand / window id, or a `monitor` persona / target |
|
|
62
|
+
| `Shift+Tab` | Move keyboard focus to the next application section (left → center → right sidebar/panel → reporting), looping; the visible tab in that section gets focus |
|
|
63
|
+
| `Enter` | Execute the current command |
|
|
64
|
+
| `Ctrl+C` | Exit |
|
|
65
|
+
|
|
66
|
+
**Image tab controls** (active only while an image tab is focused):
|
|
67
|
+
|
|
68
|
+
| Key | Action |
|
|
69
|
+
| --- | ------ |
|
|
70
|
+
| `Page Up` / scroll-wheel up | Zoom in (10% per step, up to 800%) |
|
|
71
|
+
| `Page Down` / scroll-wheel down | Zoom out (10% per step, down to 10%) |
|
|
72
|
+
| `↑` / `↓` / `←` / `→` | Pan the image |
|
|
73
|
+
| Click and drag | Pan freely |
|
|
74
|
+
| `Escape` | Reset zoom to 100% and center the view |
|
|
75
|
+
|
|
76
|
+
**Markdown tab controls** (active only while a markdown tab is focused):
|
|
77
|
+
|
|
78
|
+
| Key | Action |
|
|
79
|
+
| --- | ------ |
|
|
80
|
+
| `↑` / `↓` | Scroll up / down by a line |
|
|
81
|
+
| `Page Up` / `Page Down` | Scroll up / down by a page |
|
|
82
|
+
| Mouse wheel | Scroll up / down |
|
|
83
|
+
|
|
84
|
+
**File tree tab controls** (active only while a file tree tab is focused):
|
|
85
|
+
|
|
86
|
+
| Key | Action |
|
|
87
|
+
| --- | ------ |
|
|
88
|
+
| `↑` / `↓` | Move selection to the previous / next visible row |
|
|
89
|
+
| `→` | Collapsed directory: expand. Expanded directory: move to its first child. File: no-op |
|
|
90
|
+
| `←` | Expanded directory: collapse. Otherwise: move selection to the parent directory |
|
|
91
|
+
| `Enter` / `Space` | File: open. Directory: toggle expand/collapse |
|
|
92
|
+
| `Alt+Enter` | File: open in the plain-text editor (mirrors Alt+click) |
|
|
93
|
+
| `Home` / `End` | Select the first / last visible row |
|
|
94
|
+
| `Page Up` / `Page Down` | Move selection by one viewport of rows |
|
|
95
|
+
| Printable characters | Type-ahead: jump to the next visible row whose name starts with what's typed |
|
|
96
|
+
|
|
97
|
+
`Tab` completes the word at the cursor: filesystem paths against the tab's working directory; at the recipient position of `msg` / `broadcast`, active agent names (`broadcast` also offers `all` and completes each entry of a comma-separated list); at the target of `connection close`, the tab's open connection strings (`sqlite:<name>`, `shell:<shell>`, `acp:opencode`, `browser:<id>`); and for the `browser` command, its subcommands (`open`, `goto`, `content`, …) plus the tab's open window ids where one is expected (`browser use`, `browser window close`). For `monitor` / `unmonitor`, the first argument completes against persona names (from `ai/personas/`) and later arguments against tab labels and `group:<n>` tokens (`unmonitor` also offers `--all`).
|