santree 0.7.4 → 0.7.6
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 +2 -3
- package/dist/commands/config.d.ts +12 -0
- package/dist/commands/config.js +444 -0
- package/dist/commands/dashboard.js +187 -80
- package/dist/commands/helpers/statusline.js +0 -30
- package/dist/commands/issue/index.d.ts +1 -1
- package/dist/commands/issue/index.js +1 -1
- package/dist/commands/pr/context.d.ts +7 -0
- package/dist/commands/pr/context.js +36 -0
- package/dist/commands/pr/fix.d.ts +11 -2
- package/dist/commands/pr/fix.js +64 -12
- package/dist/commands/worktree/create.js +17 -11
- package/dist/commands/worktree/open.js +3 -2
- package/dist/commands/worktree/switch.js +3 -11
- package/dist/lib/ai.d.ts +11 -0
- package/dist/lib/ai.js +71 -2
- package/dist/lib/cd-hint.d.ts +18 -0
- package/dist/lib/cd-hint.js +27 -0
- package/dist/lib/claude-config.d.ts +26 -0
- package/dist/lib/claude-config.js +121 -0
- package/dist/lib/config/TrackerPicker.d.ts +10 -0
- package/dist/lib/config/TrackerPicker.js +113 -0
- package/dist/lib/config/diagnostics.d.ts +37 -0
- package/dist/lib/config/diagnostics.js +190 -0
- package/dist/lib/config-store.d.ts +23 -0
- package/dist/lib/config-store.js +68 -0
- package/dist/lib/dashboard/DetailPanel.d.ts +7 -2
- package/dist/lib/dashboard/DetailPanel.js +54 -37
- package/dist/lib/dashboard/IssueList.d.ts +5 -1
- package/dist/lib/dashboard/IssueList.js +24 -5
- package/dist/lib/dashboard/MultilineTextArea.js +1 -1
- package/dist/lib/dashboard/Overlays.js +3 -4
- package/dist/lib/dashboard/ReviewDetailPanel.js +11 -2
- package/dist/lib/dashboard/ReviewList.js +6 -3
- package/dist/lib/dashboard/data.js +7 -34
- package/dist/lib/dashboard/external-editor.d.ts +1 -1
- package/dist/lib/dashboard/external-editor.js +3 -2
- package/dist/lib/dashboard/types.d.ts +19 -2
- package/dist/lib/dashboard/types.js +68 -2
- package/dist/lib/exec.d.ts +13 -0
- package/dist/lib/exec.js +11 -1
- package/dist/lib/fix-loop.d.ts +45 -0
- package/dist/lib/fix-loop.js +83 -0
- package/dist/lib/git.d.ts +3 -29
- package/dist/lib/git.js +6 -62
- package/dist/lib/github.d.ts +70 -0
- package/dist/lib/github.js +128 -0
- package/dist/lib/multiplexer/cmux.js +143 -17
- package/dist/lib/multiplexer/none.js +3 -3
- package/dist/lib/multiplexer/tmux.js +5 -29
- package/dist/lib/multiplexer/types.d.ts +34 -1
- package/dist/lib/open-url.d.ts +3 -2
- package/dist/lib/open-url.js +5 -4
- package/dist/lib/prompts.d.ts +35 -1
- package/dist/lib/prompts.js +7 -0
- package/dist/lib/setup/apply.d.ts +16 -0
- package/dist/lib/setup/apply.js +35 -0
- package/dist/lib/setup/gitignore.d.ts +24 -0
- package/dist/lib/setup/gitignore.js +95 -0
- package/dist/lib/setup/steps.d.ts +39 -0
- package/dist/lib/setup/steps.js +333 -0
- package/dist/lib/setup/tools.d.ts +23 -0
- package/dist/lib/setup/tools.js +47 -0
- package/dist/lib/squirrel-loader.d.ts +3 -1
- package/dist/lib/squirrel-loader.js +2 -2
- package/dist/lib/trackers/github/index.js +2 -2
- package/dist/lib/trackers/local/store.js +1 -1
- package/package.json +1 -2
- package/prompts/fix-context.njk +79 -0
- package/prompts/fix-loop.njk +18 -0
- package/prompts/pr.njk +1 -1
- package/dist/commands/doctor.d.ts +0 -2
- package/dist/commands/doctor.js +0 -618
- package/dist/commands/github/auth.d.ts +0 -2
- package/dist/commands/github/auth.js +0 -56
- package/dist/commands/github/index.d.ts +0 -1
- package/dist/commands/github/index.js +0 -1
- package/dist/commands/helpers/english-tutor/index.d.ts +0 -1
- package/dist/commands/helpers/english-tutor/index.js +0 -1
- package/dist/commands/helpers/english-tutor/install.d.ts +0 -8
- package/dist/commands/helpers/english-tutor/install.js +0 -24
- package/dist/commands/helpers/english-tutor/prompt.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/prompt.js +0 -16
- package/dist/commands/helpers/english-tutor/session-start.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/session-start.js +0 -34
- package/dist/commands/helpers/english-tutor/uninstall.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/uninstall.js +0 -15
- package/dist/commands/helpers/session-signal/end.d.ts +0 -2
- package/dist/commands/helpers/session-signal/end.js +0 -13
- package/dist/commands/helpers/session-signal/index.d.ts +0 -1
- package/dist/commands/helpers/session-signal/index.js +0 -1
- package/dist/commands/helpers/session-signal/install.d.ts +0 -8
- package/dist/commands/helpers/session-signal/install.js +0 -24
- package/dist/commands/helpers/session-signal/notification.d.ts +0 -2
- package/dist/commands/helpers/session-signal/notification.js +0 -13
- package/dist/commands/helpers/session-signal/prompt.d.ts +0 -2
- package/dist/commands/helpers/session-signal/prompt.js +0 -13
- package/dist/commands/helpers/session-signal/stop.d.ts +0 -2
- package/dist/commands/helpers/session-signal/stop.js +0 -13
- package/dist/commands/helpers/shell-init.d.ts +0 -11
- package/dist/commands/helpers/shell-init.js +0 -122
- package/dist/commands/issue/setup.d.ts +0 -2
- package/dist/commands/issue/setup.js +0 -108
- package/dist/commands/issue/switch.d.ts +0 -12
- package/dist/commands/issue/switch.js +0 -38
- package/dist/commands/linear/switch.d.ts +0 -2
- package/dist/commands/linear/switch.js +0 -75
- package/dist/lib/english-tutor.d.ts +0 -13
- package/dist/lib/english-tutor.js +0 -125
- package/dist/lib/session-signal.d.ts +0 -16
- package/dist/lib/session-signal.js +0 -104
- package/prompts/english-tutor-prompt.njk +0 -15
- package/shell/init.bash.njk +0 -129
- package/shell/init.zsh.njk +0 -204
package/dist/lib/git.d.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
export { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
|
|
2
|
-
export interface SessionState {
|
|
3
|
-
state: "waiting" | "idle" | "active" | "exited";
|
|
4
|
-
message: string | null;
|
|
5
|
-
session_id: string;
|
|
6
|
-
at: string;
|
|
7
|
-
}
|
|
8
2
|
export interface Worktree {
|
|
9
3
|
path: string;
|
|
10
4
|
branch: string;
|
|
@@ -191,17 +185,14 @@ export declare function getCommitsAheadAsync(cwd: string, baseBranch: string): P
|
|
|
191
185
|
*/
|
|
192
186
|
export declare function getCommitsBehindAsync(cwd: string, baseBranch: string): Promise<number>;
|
|
193
187
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
188
|
+
* Resolve the configured diff pager command (e.g. "delta", "diff-so-fancy"),
|
|
189
|
+
* or null if unset/invalid. Source order: `SANTREE_DIFF_TOOL` env override →
|
|
190
|
+
* santree's config file (set from `santree config`). See `lib/config-store.ts`.
|
|
196
191
|
*
|
|
197
192
|
* The CLI `worktree diff` flow lets the pager do its full job (render +
|
|
198
193
|
* scroll) via git's `core.pager`. The dashboard's `[v]` overlay only uses
|
|
199
194
|
* the rendering half — it captures the pager's stdout as a string and
|
|
200
195
|
* handles scrolling itself in Ink.
|
|
201
|
-
*
|
|
202
|
-
* The value is restricted to a safe shell-token character set since it ends
|
|
203
|
-
* up in arguments passed to spawn() — even though we never use shell:true,
|
|
204
|
-
* keeping the surface tight defends against accidental misconfigurations.
|
|
205
196
|
*/
|
|
206
197
|
export declare function getDiffTool(): string | null;
|
|
207
198
|
export interface DiffShortstat {
|
|
@@ -315,20 +306,3 @@ export declare function unstageAll(cwd: string): Promise<void>;
|
|
|
315
306
|
* - Untracked: deletes the file from disk via fs.unlink.
|
|
316
307
|
*/
|
|
317
308
|
export declare function discardFile(cwd: string, filePath: string, isUntracked: boolean): Promise<void>;
|
|
318
|
-
/**
|
|
319
|
-
* Read the session state file for a given ticket.
|
|
320
|
-
* Returns null if missing or "exited".
|
|
321
|
-
*/
|
|
322
|
-
export declare function readSessionState(repoRoot: string, ticketId: string): SessionState | null;
|
|
323
|
-
/**
|
|
324
|
-
* Check if a session for the given ticket is still alive in the active multiplexer.
|
|
325
|
-
* Delegates to the configured multiplexer (tmux: pane_pid + pgrep claude;
|
|
326
|
-
* cmux: workspace lookup; none: always false). Callers should also consult the
|
|
327
|
-
* .santree/session-states/<ticketId>.json file — that's the authoritative signal
|
|
328
|
-
* written by Claude Code hooks, while this is the "is the terminal still up" backstop.
|
|
329
|
-
*/
|
|
330
|
-
export declare function isSessionAlive(ticketId: string): boolean;
|
|
331
|
-
/**
|
|
332
|
-
* Delete the session state file for a given ticket.
|
|
333
|
-
*/
|
|
334
|
-
export declare function clearSessionState(repoRoot: string, ticketId: string): void;
|
package/dist/lib/git.js
CHANGED
|
@@ -3,7 +3,7 @@ import { promisify } from "util";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import { run, runAsync, spawnAsync } from "./exec.js";
|
|
6
|
-
import {
|
|
6
|
+
import { getConfiguredDiffTool } from "./config-store.js";
|
|
7
7
|
import { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
|
|
8
8
|
import { getIssueTracker } from "./trackers/index.js";
|
|
9
9
|
export { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
|
|
@@ -217,7 +217,7 @@ export async function removeWorktree(branchName, repoRoot, force = false, onProg
|
|
|
217
217
|
}
|
|
218
218
|
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
219
219
|
}
|
|
220
|
-
// Clean up centralized metadata entry
|
|
220
|
+
// Clean up centralized metadata entry
|
|
221
221
|
const ticketId = extractTicketId(branchName);
|
|
222
222
|
if (ticketId) {
|
|
223
223
|
const all = readAllMetadata(repoRoot);
|
|
@@ -225,7 +225,6 @@ export async function removeWorktree(branchName, repoRoot, force = false, onProg
|
|
|
225
225
|
delete all[ticketId];
|
|
226
226
|
writeAllMetadata(repoRoot, all);
|
|
227
227
|
}
|
|
228
|
-
clearSessionState(repoRoot, ticketId);
|
|
229
228
|
}
|
|
230
229
|
// Also delete the branch
|
|
231
230
|
report("Deleting branch…");
|
|
@@ -457,26 +456,17 @@ export async function getCommitsBehindAsync(cwd, baseBranch) {
|
|
|
457
456
|
return output ? parseInt(output, 10) || 0 : 0;
|
|
458
457
|
}
|
|
459
458
|
/**
|
|
460
|
-
*
|
|
461
|
-
*
|
|
459
|
+
* Resolve the configured diff pager command (e.g. "delta", "diff-so-fancy"),
|
|
460
|
+
* or null if unset/invalid. Source order: `SANTREE_DIFF_TOOL` env override →
|
|
461
|
+
* santree's config file (set from `santree config`). See `lib/config-store.ts`.
|
|
462
462
|
*
|
|
463
463
|
* The CLI `worktree diff` flow lets the pager do its full job (render +
|
|
464
464
|
* scroll) via git's `core.pager`. The dashboard's `[v]` overlay only uses
|
|
465
465
|
* the rendering half — it captures the pager's stdout as a string and
|
|
466
466
|
* handles scrolling itself in Ink.
|
|
467
|
-
*
|
|
468
|
-
* The value is restricted to a safe shell-token character set since it ends
|
|
469
|
-
* up in arguments passed to spawn() — even though we never use shell:true,
|
|
470
|
-
* keeping the surface tight defends against accidental misconfigurations.
|
|
471
467
|
*/
|
|
472
468
|
export function getDiffTool() {
|
|
473
|
-
|
|
474
|
-
if (!raw || !raw.trim())
|
|
475
|
-
return null;
|
|
476
|
-
const tool = raw.trim();
|
|
477
|
-
if (!/^[a-zA-Z0-9_\-/.+]+$/.test(tool))
|
|
478
|
-
return null;
|
|
479
|
-
return tool;
|
|
469
|
+
return getConfiguredDiffTool() ?? null;
|
|
480
470
|
}
|
|
481
471
|
/**
|
|
482
472
|
* Compute branch-only diff stats vs base (uses merge-base, like a GitHub PR
|
|
@@ -704,49 +694,3 @@ export async function discardFile(cwd, filePath, isUntracked) {
|
|
|
704
694
|
throw new Error(`git checkout HEAD failed: ${output.trim()}`);
|
|
705
695
|
}
|
|
706
696
|
}
|
|
707
|
-
/**
|
|
708
|
-
* Get the path to the .santree/session-states directory.
|
|
709
|
-
*/
|
|
710
|
-
function getSessionStatesDir(repoRoot) {
|
|
711
|
-
return path.join(getSantreeDir(repoRoot), "session-states");
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Read the session state file for a given ticket.
|
|
715
|
-
* Returns null if missing or "exited".
|
|
716
|
-
*/
|
|
717
|
-
export function readSessionState(repoRoot, ticketId) {
|
|
718
|
-
const filePath = path.join(getSessionStatesDir(repoRoot), `${ticketId}.json`);
|
|
719
|
-
if (!fs.existsSync(filePath))
|
|
720
|
-
return null;
|
|
721
|
-
try {
|
|
722
|
-
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
723
|
-
if (data.state === "exited")
|
|
724
|
-
return null;
|
|
725
|
-
return data;
|
|
726
|
-
}
|
|
727
|
-
catch {
|
|
728
|
-
return null;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
/**
|
|
732
|
-
* Check if a session for the given ticket is still alive in the active multiplexer.
|
|
733
|
-
* Delegates to the configured multiplexer (tmux: pane_pid + pgrep claude;
|
|
734
|
-
* cmux: workspace lookup; none: always false). Callers should also consult the
|
|
735
|
-
* .santree/session-states/<ticketId>.json file — that's the authoritative signal
|
|
736
|
-
* written by Claude Code hooks, while this is the "is the terminal still up" backstop.
|
|
737
|
-
*/
|
|
738
|
-
export function isSessionAlive(ticketId) {
|
|
739
|
-
return getMultiplexer().isSessionAlive(ticketId);
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* Delete the session state file for a given ticket.
|
|
743
|
-
*/
|
|
744
|
-
export function clearSessionState(repoRoot, ticketId) {
|
|
745
|
-
const filePath = path.join(getSessionStatesDir(repoRoot), `${ticketId}.json`);
|
|
746
|
-
try {
|
|
747
|
-
fs.unlinkSync(filePath);
|
|
748
|
-
}
|
|
749
|
-
catch {
|
|
750
|
-
// Ignore if file doesn't exist
|
|
751
|
-
}
|
|
752
|
-
}
|
package/dist/lib/github.d.ts
CHANGED
|
@@ -10,6 +10,34 @@ export interface PRInfo {
|
|
|
10
10
|
* Returns null if no PR exists for the branch or gh CLI fails.
|
|
11
11
|
*/
|
|
12
12
|
export declare function getPRInfoAsync(branchName: string): Promise<PRInfo | null>;
|
|
13
|
+
/** GitHub's mergeability signals for a PR (subset we act on). */
|
|
14
|
+
export interface PRMergeState {
|
|
15
|
+
/** "MERGEABLE" | "CONFLICTING" | "UNKNOWN" */
|
|
16
|
+
mergeable: string;
|
|
17
|
+
/** "BEHIND" | "BLOCKED" | "CLEAN" | "DIRTY" | "DRAFT" | "HAS_HOOKS" | "UNKNOWN" | "UNSTABLE" */
|
|
18
|
+
mergeStateStatus: string;
|
|
19
|
+
/** True when the branch has conflicts with its base that need resolving. */
|
|
20
|
+
hasConflicts: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fetch a PR's mergeability for a branch. `CONFLICTING` (or a `DIRTY` merge-state)
|
|
24
|
+
* means the branch conflicts with its base and needs a merge/resolve before CI can
|
|
25
|
+
* pass. Returns null if no PR exists or gh fails. GitHub computes mergeability
|
|
26
|
+
* asynchronously, so `mergeable` is `UNKNOWN` briefly after a push — callers should
|
|
27
|
+
* treat `UNKNOWN` as "don't know yet", not "no conflicts".
|
|
28
|
+
*/
|
|
29
|
+
export declare function getPRMergeStateAsync(branchName: string): Promise<PRMergeState | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Classify a CI check as something an AI agent can plausibly fix on its own
|
|
32
|
+
* ("fixable" — tests, types, lint, format, coverage) vs. one that needs a human or
|
|
33
|
+
* infra ("manual" — deploys, releases, image builds, e2e, security scans). Matched
|
|
34
|
+
* case-insensitively against the check's name + workflow. **Unknown ⇒ "manual"**:
|
|
35
|
+
* the fix loop should only touch failures it's confident about and stop otherwise.
|
|
36
|
+
* Extend the keyword lists as new check conventions appear (cf. HIDDEN_STATE_NAMES).
|
|
37
|
+
*/
|
|
38
|
+
export declare const FIXABLE_CHECK_PATTERN: RegExp;
|
|
39
|
+
export declare const MANUAL_CHECK_PATTERN: RegExp;
|
|
40
|
+
export declare function classifyCheck(check: Pick<PRCheck, "name" | "workflow">): "fixable" | "manual";
|
|
13
41
|
/**
|
|
14
42
|
* Check if the GitHub CLI (gh) is available on PATH.
|
|
15
43
|
* Runs: `which gh`
|
|
@@ -50,6 +78,48 @@ export declare function getPRReviewsAsync(prNumber: string): Promise<PRReview[]
|
|
|
50
78
|
* Fetch inline review comments for a pull request via the GitHub API (async).
|
|
51
79
|
*/
|
|
52
80
|
export declare function getPRReviewCommentsAsync(prNumber: string): Promise<PRReviewComment[] | null>;
|
|
81
|
+
/**
|
|
82
|
+
* The authenticated GitHub user's login (GraphQL `viewer`). Cached per process.
|
|
83
|
+
* Used to gate auto-applying a review comment on the PR owner's OWN 👍 — a
|
|
84
|
+
* reviewer can't self-approve their comment for the fix loop.
|
|
85
|
+
*/
|
|
86
|
+
export declare function getViewerLoginAsync(): Promise<string | null>;
|
|
87
|
+
export interface PRReviewThreadComment {
|
|
88
|
+
author: string;
|
|
89
|
+
body: string;
|
|
90
|
+
path: string | null;
|
|
91
|
+
/** Current line, falling back to the original line for outdated comments. */
|
|
92
|
+
line: number | null;
|
|
93
|
+
diffHunk: string;
|
|
94
|
+
createdAt: string;
|
|
95
|
+
/** Logins of users who reacted 👍 (THUMBS_UP) to this comment. */
|
|
96
|
+
thumbsUpBy: string[];
|
|
97
|
+
}
|
|
98
|
+
/** A PR review thread (an inline comment + its replies) with resolution state. */
|
|
99
|
+
export interface PRReviewThread {
|
|
100
|
+
/** GraphQL node id — used to resolve the thread after the fix is applied. */
|
|
101
|
+
id: string;
|
|
102
|
+
isResolved: boolean;
|
|
103
|
+
isOutdated: boolean;
|
|
104
|
+
path: string | null;
|
|
105
|
+
/** Anchor line in the current diff, falling back to the original line. */
|
|
106
|
+
line: number | null;
|
|
107
|
+
comments: PRReviewThreadComment[];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Fetch a PR's review threads with resolution state and per-comment 👍 reactions.
|
|
111
|
+
* The REST comments endpoint exposes neither, so this goes through GraphQL.
|
|
112
|
+
* Returns null if the repo can't be resolved or gh fails.
|
|
113
|
+
*/
|
|
114
|
+
export declare function getPRReviewThreadsAsync(prNumber: string): Promise<PRReviewThread[] | null>;
|
|
115
|
+
/**
|
|
116
|
+
* Filter review threads to the ones the fix loop should act on: **unresolved**
|
|
117
|
+
* AND whose **last comment carries a 👍 from `approver`** (the viewer's own
|
|
118
|
+
* approval). Resolved threads are dropped entirely — resolved means the ask was
|
|
119
|
+
* already applied or deliberately declined. The 👍 gate stops the loop from
|
|
120
|
+
* applying a comment the moment it's left, before the PR owner has vetted it.
|
|
121
|
+
*/
|
|
122
|
+
export declare function getApprovedReviewThreads(threads: PRReviewThread[], approver: string): PRReviewThread[];
|
|
53
123
|
/**
|
|
54
124
|
* Fetch structured conversation comments on a pull request (async).
|
|
55
125
|
*/
|
package/dist/lib/github.js
CHANGED
|
@@ -22,6 +22,50 @@ export async function getPRInfoAsync(branchName) {
|
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Fetch a PR's mergeability for a branch. `CONFLICTING` (or a `DIRTY` merge-state)
|
|
27
|
+
* means the branch conflicts with its base and needs a merge/resolve before CI can
|
|
28
|
+
* pass. Returns null if no PR exists or gh fails. GitHub computes mergeability
|
|
29
|
+
* asynchronously, so `mergeable` is `UNKNOWN` briefly after a push — callers should
|
|
30
|
+
* treat `UNKNOWN` as "don't know yet", not "no conflicts".
|
|
31
|
+
*/
|
|
32
|
+
export async function getPRMergeStateAsync(branchName) {
|
|
33
|
+
try {
|
|
34
|
+
const { stdout } = await execAsync(`gh pr view "${branchName}" --json mergeable,mergeStateStatus`);
|
|
35
|
+
const data = JSON.parse(stdout);
|
|
36
|
+
const mergeable = String(data.mergeable ?? "UNKNOWN");
|
|
37
|
+
const mergeStateStatus = String(data.mergeStateStatus ?? "UNKNOWN");
|
|
38
|
+
return {
|
|
39
|
+
mergeable,
|
|
40
|
+
mergeStateStatus,
|
|
41
|
+
hasConflicts: mergeable === "CONFLICTING" || mergeStateStatus === "DIRTY",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Classify a CI check as something an AI agent can plausibly fix on its own
|
|
50
|
+
* ("fixable" — tests, types, lint, format, coverage) vs. one that needs a human or
|
|
51
|
+
* infra ("manual" — deploys, releases, image builds, e2e, security scans). Matched
|
|
52
|
+
* case-insensitively against the check's name + workflow. **Unknown ⇒ "manual"**:
|
|
53
|
+
* the fix loop should only touch failures it's confident about and stop otherwise.
|
|
54
|
+
* Extend the keyword lists as new check conventions appear (cf. HIDDEN_STATE_NAMES).
|
|
55
|
+
*/
|
|
56
|
+
// Trailing \b only (no leading boundary) so test-runner prefixes and plurals
|
|
57
|
+
// match: "pytest", "tests", "rspec", "codecov" all count as fixable.
|
|
58
|
+
export const FIXABLE_CHECK_PATTERN = /(tests?|specs?|jest|vitest|mocha|junit|phpunit|lint|format|prettier|eslint|ruff|black|gofmt|rustfmt|type[\s-]?check|tsc|mypy|pyright|coverage|cov)\b/i;
|
|
59
|
+
export const MANUAL_CHECK_PATTERN = /\b(deploy|release|publish|build[\s-]?image|docker|e2e|end[\s-]?to[\s-]?end|integration|smoke|terraform|infra|migrat|codeql|security|scan|sign|notariz)\b/i;
|
|
60
|
+
export function classifyCheck(check) {
|
|
61
|
+
const haystack = `${check.name} ${check.workflow}`;
|
|
62
|
+
// Manual wins ties: a "deploy tests" job is safer treated as manual.
|
|
63
|
+
if (MANUAL_CHECK_PATTERN.test(haystack))
|
|
64
|
+
return "manual";
|
|
65
|
+
if (FIXABLE_CHECK_PATTERN.test(haystack))
|
|
66
|
+
return "fixable";
|
|
67
|
+
return "manual";
|
|
68
|
+
}
|
|
25
69
|
/**
|
|
26
70
|
* Check if the GitHub CLI (gh) is available on PATH.
|
|
27
71
|
* Runs: `which gh`
|
|
@@ -137,6 +181,90 @@ export async function getPRReviewCommentsAsync(prNumber) {
|
|
|
137
181
|
return null;
|
|
138
182
|
}
|
|
139
183
|
}
|
|
184
|
+
/** Per-process cache of the authenticated GitHub login (the "viewer"). */
|
|
185
|
+
let viewerLoginCache = null;
|
|
186
|
+
/**
|
|
187
|
+
* The authenticated GitHub user's login (GraphQL `viewer`). Cached per process.
|
|
188
|
+
* Used to gate auto-applying a review comment on the PR owner's OWN 👍 — a
|
|
189
|
+
* reviewer can't self-approve their comment for the fix loop.
|
|
190
|
+
*/
|
|
191
|
+
export function getViewerLoginAsync() {
|
|
192
|
+
if (viewerLoginCache)
|
|
193
|
+
return viewerLoginCache;
|
|
194
|
+
viewerLoginCache = (async () => {
|
|
195
|
+
try {
|
|
196
|
+
const { stdout } = await execAsync(`gh api graphql -f query='{ viewer { login } }' --jq .data.viewer.login`);
|
|
197
|
+
return stdout.trim() || null;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
})();
|
|
203
|
+
return viewerLoginCache;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Fetch a PR's review threads with resolution state and per-comment 👍 reactions.
|
|
207
|
+
* The REST comments endpoint exposes neither, so this goes through GraphQL.
|
|
208
|
+
* Returns null if the repo can't be resolved or gh fails.
|
|
209
|
+
*/
|
|
210
|
+
export async function getPRReviewThreadsAsync(prNumber) {
|
|
211
|
+
const repo = await getRepoNameAsync();
|
|
212
|
+
if (!repo)
|
|
213
|
+
return null;
|
|
214
|
+
const [owner, name] = repo.split("/");
|
|
215
|
+
if (!owner || !name)
|
|
216
|
+
return null;
|
|
217
|
+
// Single-line query (GraphQL ignores whitespace); single-quoted for the shell.
|
|
218
|
+
// `first` bounds keep us well under GitHub's query-complexity cap.
|
|
219
|
+
const query = `query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){` +
|
|
220
|
+
`pullRequest(number:$number){reviewThreads(first:100){nodes{id isResolved isOutdated path line originalLine ` +
|
|
221
|
+
`comments(first:50){nodes{author{login} body path line originalLine diffHunk createdAt ` +
|
|
222
|
+
`reactions(first:50){nodes{content user{login}}}}}}}}}}`;
|
|
223
|
+
const output = await runAsync(`gh api graphql -f query='${query}' -f owner='${owner}' -f name='${name}' -F number=${Number(prNumber)}`);
|
|
224
|
+
if (!output)
|
|
225
|
+
return null;
|
|
226
|
+
try {
|
|
227
|
+
const data = JSON.parse(output);
|
|
228
|
+
const nodes = data?.data?.repository?.pullRequest?.reviewThreads?.nodes ?? [];
|
|
229
|
+
return nodes.map((t) => ({
|
|
230
|
+
id: String(t.id),
|
|
231
|
+
isResolved: !!t.isResolved,
|
|
232
|
+
isOutdated: !!t.isOutdated,
|
|
233
|
+
path: t.path ?? null,
|
|
234
|
+
line: t.line ?? t.originalLine ?? null,
|
|
235
|
+
comments: (t.comments?.nodes ?? []).map((c) => ({
|
|
236
|
+
author: c.author?.login ?? "unknown",
|
|
237
|
+
body: c.body ?? "",
|
|
238
|
+
path: c.path ?? null,
|
|
239
|
+
line: c.line ?? c.originalLine ?? null,
|
|
240
|
+
diffHunk: c.diffHunk ?? "",
|
|
241
|
+
createdAt: c.createdAt ?? "",
|
|
242
|
+
thumbsUpBy: (c.reactions?.nodes ?? [])
|
|
243
|
+
.filter((r) => r.content === "THUMBS_UP")
|
|
244
|
+
.map((r) => r.user?.login)
|
|
245
|
+
.filter(Boolean),
|
|
246
|
+
})),
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Filter review threads to the ones the fix loop should act on: **unresolved**
|
|
255
|
+
* AND whose **last comment carries a 👍 from `approver`** (the viewer's own
|
|
256
|
+
* approval). Resolved threads are dropped entirely — resolved means the ask was
|
|
257
|
+
* already applied or deliberately declined. The 👍 gate stops the loop from
|
|
258
|
+
* applying a comment the moment it's left, before the PR owner has vetted it.
|
|
259
|
+
*/
|
|
260
|
+
export function getApprovedReviewThreads(threads, approver) {
|
|
261
|
+
return threads.filter((t) => {
|
|
262
|
+
if (t.isResolved)
|
|
263
|
+
return false;
|
|
264
|
+
const last = t.comments[t.comments.length - 1];
|
|
265
|
+
return !!last && last.thumbsUpBy.includes(approver);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
140
268
|
/**
|
|
141
269
|
* Fetch structured conversation comments on a pull request (async).
|
|
142
270
|
*/
|
|
@@ -28,16 +28,112 @@ function findWorkspaceByTitle(title) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
// `list-pane-surfaces --workspace X` reports one pane (the active one) and its
|
|
32
|
+
// surfaces (tabs). Enough for our two flows: the work workspace has a single
|
|
33
|
+
// pane, and a freshly-created temp workspace has a single pane + single surface.
|
|
34
|
+
function paneSurfaces(workspaceRef) {
|
|
35
|
+
const r = cmuxRun(`cmux --json list-pane-surfaces --workspace ${shellEscape(workspaceRef)}`);
|
|
36
|
+
if (!r.ok)
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(r.stdout);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function renameTab(workspaceRef, surfaceRef, title) {
|
|
46
|
+
cmuxRun(`cmux rename-tab --workspace ${shellEscape(workspaceRef)} --surface ${shellEscape(surfaceRef)} ${shellEscape(title)}`);
|
|
47
|
+
}
|
|
48
|
+
// cmux exposes workspace-group (sidebar folder) operations only over the RPC
|
|
49
|
+
// socket, not as first-class subcommands. `cmux rpc <method> '<json>'`.
|
|
50
|
+
function cmuxRpc(method, params) {
|
|
51
|
+
const arg = params ? ` ${shellEscape(JSON.stringify(params))}` : "";
|
|
52
|
+
const result = cmuxRun(`cmux rpc ${method}${arg}`);
|
|
53
|
+
if (!result.ok)
|
|
54
|
+
return { ok: false };
|
|
55
|
+
try {
|
|
56
|
+
return { ok: true, data: JSON.parse(result.stdout) };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { ok: false };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function findGroupIdByName(name) {
|
|
63
|
+
const r = cmuxRpc("workspace.group.list");
|
|
64
|
+
if (!r.ok)
|
|
65
|
+
return null;
|
|
66
|
+
const groups = r.data?.groups ?? [];
|
|
67
|
+
return groups.find((g) => g.name === name)?.id ?? null;
|
|
68
|
+
}
|
|
69
|
+
// Snapshot every workspace's current group membership (ref -> group id), so we
|
|
70
|
+
// can detect and undo the side effect described in `createGroup` below.
|
|
71
|
+
function snapshotMemberships() {
|
|
72
|
+
const map = new Map();
|
|
73
|
+
const r = cmuxRpc("workspace.group.list");
|
|
74
|
+
if (!r.ok)
|
|
75
|
+
return map;
|
|
76
|
+
for (const g of r.data?.groups ?? []) {
|
|
77
|
+
if (!g.id)
|
|
78
|
+
continue;
|
|
79
|
+
for (const ref of g.member_workspace_refs ?? [])
|
|
80
|
+
map.set(ref, g.id);
|
|
81
|
+
}
|
|
82
|
+
return map;
|
|
83
|
+
}
|
|
84
|
+
// Create a sidebar group (folder) named `name` and return its id.
|
|
85
|
+
//
|
|
86
|
+
// Two cmux quirks shape this:
|
|
87
|
+
// 1. A group renders as a folder whose header IS its "anchor" workspace
|
|
88
|
+
// (auto-titled with the group name); members nest under it. We must keep
|
|
89
|
+
// that anchor — closing it removes the folder label and cmux flattens the
|
|
90
|
+
// members back to top-level rows (visually ungrouped).
|
|
91
|
+
// 2. `workspace.group.create` also pulls the *currently-selected* workspace
|
|
92
|
+
// into the new group as a stray member. We undo that: restore each stray to
|
|
93
|
+
// whatever group it was in before (or detach it if none), so creating a
|
|
94
|
+
// project folder never disturbs the user's existing folders or their
|
|
95
|
+
// dashboard window.
|
|
96
|
+
function createGroup(name) {
|
|
97
|
+
const before = snapshotMemberships();
|
|
98
|
+
const created = cmuxRpc("workspace.group.create", { name });
|
|
99
|
+
if (!created.ok)
|
|
100
|
+
return null;
|
|
101
|
+
const group = created.data?.group;
|
|
102
|
+
if (!group?.id)
|
|
103
|
+
return null;
|
|
104
|
+
const anchor = group.anchor_workspace_ref;
|
|
105
|
+
for (const ref of group.member_workspace_refs ?? []) {
|
|
106
|
+
if (ref === anchor)
|
|
107
|
+
continue;
|
|
108
|
+
const prev = before.get(ref);
|
|
109
|
+
if (prev && prev !== group.id) {
|
|
110
|
+
cmuxRpc("workspace.group.add", { group_id: prev, workspace_id: ref });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
cmuxRpc("workspace.group.remove", { group_id: group.id, workspace_id: ref });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return group.id;
|
|
117
|
+
}
|
|
118
|
+
// Place an already-created workspace into a sidebar group named `groupName`,
|
|
119
|
+
// creating the group on first use. Best-effort: any failure leaves the
|
|
120
|
+
// workspace ungrouped but otherwise intact.
|
|
121
|
+
function placeWorkspaceInGroup(groupName, workspaceRef) {
|
|
122
|
+
const groupId = findGroupIdByName(groupName) ?? createGroup(groupName);
|
|
123
|
+
if (!groupId)
|
|
124
|
+
return;
|
|
125
|
+
cmuxRpc("workspace.group.add", { group_id: groupId, workspace_id: workspaceRef });
|
|
126
|
+
}
|
|
31
127
|
export const cmuxMultiplexer = {
|
|
32
128
|
kind: "cmux",
|
|
33
129
|
isActive() {
|
|
34
130
|
return !!process.env["CMUX_SURFACE_ID"];
|
|
35
131
|
},
|
|
36
|
-
async createWindow({ name, cwd, command }) {
|
|
132
|
+
async createWindow({ name, cwd, command, group, tabName, }) {
|
|
37
133
|
// `new-workspace` accepts --name, --cwd, --command in a single atomic call.
|
|
38
|
-
// `--command` sends "<text>\n" to the new surface after creation
|
|
39
|
-
//
|
|
40
|
-
//
|
|
134
|
+
// `--command` sends "<text>\n" to the new surface after creation, which IS
|
|
135
|
+
// the one reliable way to get a live PTY (cmux #1472 only breaks commands on
|
|
136
|
+
// surfaces created *after* the workspace exists).
|
|
41
137
|
const parts = [`cmux new-workspace --name ${shellEscape(name)} --cwd ${shellEscape(cwd)}`];
|
|
42
138
|
if (command)
|
|
43
139
|
parts.push(`--command ${shellEscape(command)}`);
|
|
@@ -45,6 +141,49 @@ export const cmuxMultiplexer = {
|
|
|
45
141
|
if (!created.ok) {
|
|
46
142
|
return { ok: false, reason: "failed", message: "cmux new-workspace failed" };
|
|
47
143
|
}
|
|
144
|
+
// Grouping / tab-naming are best-effort and must never fail the launch.
|
|
145
|
+
// `new-workspace` prints the new workspace ref (`OK workspace:N`) to stdout.
|
|
146
|
+
const ref = created.stdout.match(/workspace:\d+/)?.[0];
|
|
147
|
+
if (group && ref)
|
|
148
|
+
placeWorkspaceInGroup(group, ref);
|
|
149
|
+
if (tabName && ref) {
|
|
150
|
+
const surface = paneSurfaces(ref)?.surfaces?.[0]?.ref;
|
|
151
|
+
if (surface)
|
|
152
|
+
renameTab(ref, surface, tabName);
|
|
153
|
+
}
|
|
154
|
+
return { ok: true };
|
|
155
|
+
},
|
|
156
|
+
async addTab({ windowName, tabName, cwd, command, group }) {
|
|
157
|
+
const target = findWorkspaceByTitle(windowName);
|
|
158
|
+
// No existing workspace for this ticket — create one named after it, with
|
|
159
|
+
// the command as its (renamed) tab.
|
|
160
|
+
if (!target?.ref) {
|
|
161
|
+
return this.createWindow({ name: windowName, cwd, command, group, tabName });
|
|
162
|
+
}
|
|
163
|
+
const targetPane = paneSurfaces(target.ref)?.pane_ref;
|
|
164
|
+
if (!targetPane) {
|
|
165
|
+
return { ok: false, reason: "failed", message: "could not resolve target pane" };
|
|
166
|
+
}
|
|
167
|
+
// A surface only gets a live PTY when its command is baked in at workspace
|
|
168
|
+
// creation. So we spawn the command in a throwaway workspace, then relocate
|
|
169
|
+
// the (already-running) surface into the target pane and discard the husk.
|
|
170
|
+
const tmpName = `__santree_tab_${windowName}`;
|
|
171
|
+
const tmp = cmuxRun(`cmux new-workspace --name ${shellEscape(tmpName)} --cwd ${shellEscape(cwd)} --command ${shellEscape(command)}`);
|
|
172
|
+
if (!tmp.ok) {
|
|
173
|
+
return { ok: false, reason: "failed", message: "could not launch tab command" };
|
|
174
|
+
}
|
|
175
|
+
const tmpRef = tmp.stdout.match(/workspace:\d+/)?.[0];
|
|
176
|
+
const surface = tmpRef ? paneSurfaces(tmpRef)?.surfaces?.[0]?.ref : undefined;
|
|
177
|
+
if (!tmpRef || !surface) {
|
|
178
|
+
return { ok: false, reason: "failed", message: "could not resolve new tab surface" };
|
|
179
|
+
}
|
|
180
|
+
const moved = cmuxRun(`cmux move-surface --surface ${shellEscape(surface)} --pane ${shellEscape(targetPane)}`);
|
|
181
|
+
if (!moved.ok) {
|
|
182
|
+
// Leave the command running in its own workspace rather than losing it.
|
|
183
|
+
return { ok: false, reason: "failed", message: "could not move tab into workspace" };
|
|
184
|
+
}
|
|
185
|
+
cmuxRun(`cmux workspace close --workspace ${shellEscape(tmpRef)}`);
|
|
186
|
+
renameTab(target.ref, surface, tabName);
|
|
48
187
|
return { ok: true };
|
|
49
188
|
},
|
|
50
189
|
async selectWindow(name) {
|
|
@@ -66,17 +205,4 @@ export const cmuxMultiplexer = {
|
|
|
66
205
|
message: "blocked by manaflow-ai/cmux#1472",
|
|
67
206
|
};
|
|
68
207
|
},
|
|
69
|
-
isSessionAlive(ticketId) {
|
|
70
|
-
const result = cmuxRun("cmux --json list-workspaces");
|
|
71
|
-
if (!result.ok)
|
|
72
|
-
return false;
|
|
73
|
-
try {
|
|
74
|
-
const parsed = JSON.parse(result.stdout);
|
|
75
|
-
const items = parsed.workspaces ?? [];
|
|
76
|
-
return items.some((w) => typeof w.title === "string" && w.title.startsWith(ticketId));
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
208
|
};
|
|
@@ -7,13 +7,13 @@ export const noneMultiplexer = {
|
|
|
7
7
|
async createWindow() {
|
|
8
8
|
return NOT_ACTIVE;
|
|
9
9
|
},
|
|
10
|
+
async addTab() {
|
|
11
|
+
return NOT_ACTIVE;
|
|
12
|
+
},
|
|
10
13
|
async selectWindow() {
|
|
11
14
|
return NOT_ACTIVE;
|
|
12
15
|
},
|
|
13
16
|
sendCommand() {
|
|
14
17
|
return NOT_ACTIVE;
|
|
15
18
|
},
|
|
16
|
-
isSessionAlive() {
|
|
17
|
-
return false;
|
|
18
|
-
},
|
|
19
19
|
};
|
|
@@ -30,6 +30,11 @@ export const tmuxMultiplexer = {
|
|
|
30
30
|
}
|
|
31
31
|
return { ok: true };
|
|
32
32
|
},
|
|
33
|
+
async addTab({ windowName, tabName, cwd, command }) {
|
|
34
|
+
// tmux has no in-window tab concept, so a "tab" becomes its own window named
|
|
35
|
+
// `<tabName>-<windowName>` (e.g. fix-loop-TEAM-123).
|
|
36
|
+
return this.createWindow({ name: `${tabName}-${windowName}`, cwd, command });
|
|
37
|
+
},
|
|
33
38
|
async selectWindow(name) {
|
|
34
39
|
if (!this.isActive())
|
|
35
40
|
return { ok: false, reason: "not-active" };
|
|
@@ -42,33 +47,4 @@ export const tmuxMultiplexer = {
|
|
|
42
47
|
const ok = tmuxSync(`tmux send-keys -t ${shellEscape(name)} ${shellEscape(command)} Enter`);
|
|
43
48
|
return ok ? { ok: true } : { ok: false, reason: "failed" };
|
|
44
49
|
},
|
|
45
|
-
isSessionAlive(ticketId) {
|
|
46
|
-
try {
|
|
47
|
-
const output = execSync('tmux list-windows -F "#{window_name}\t#{pane_pid}"', {
|
|
48
|
-
encoding: "utf-8",
|
|
49
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
50
|
-
}).trim();
|
|
51
|
-
for (const line of output.split("\n")) {
|
|
52
|
-
const [name, pidStr] = line.split("\t");
|
|
53
|
-
if (!name?.startsWith(ticketId))
|
|
54
|
-
continue;
|
|
55
|
-
if (!pidStr)
|
|
56
|
-
return false;
|
|
57
|
-
try {
|
|
58
|
-
const ps = execSync(`pgrep -P ${pidStr} -a`, {
|
|
59
|
-
encoding: "utf-8",
|
|
60
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
61
|
-
}).trim();
|
|
62
|
-
return ps.split("\n").some((proc) => proc.includes("claude"));
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// tmux not available
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
},
|
|
74
50
|
};
|
|
@@ -10,13 +10,46 @@ export interface CreateWindowOpts {
|
|
|
10
10
|
name: string;
|
|
11
11
|
cwd: string;
|
|
12
12
|
command?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Optional sidebar group/folder to place the new window in. Backends that
|
|
15
|
+
* have no grouping concept (tmux, none) ignore it; cmux files the workspace
|
|
16
|
+
* under a group of this name (creating it on first use). Used to cluster all
|
|
17
|
+
* windows for one Linear project together.
|
|
18
|
+
*/
|
|
19
|
+
group?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Optional name for the window's initial tab/surface. cmux renames the tab
|
|
22
|
+
* after creation; backends without an in-window tab concept ignore it.
|
|
23
|
+
*/
|
|
24
|
+
tabName?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface AddTabOpts {
|
|
27
|
+
/** Name of the existing window/workspace to add the tab to. */
|
|
28
|
+
windowName: string;
|
|
29
|
+
/** Name for the new tab. */
|
|
30
|
+
tabName: string;
|
|
31
|
+
cwd: string;
|
|
32
|
+
command: string;
|
|
33
|
+
/**
|
|
34
|
+
* Group to use only if the target window doesn't exist yet and a fresh one
|
|
35
|
+
* has to be created as a fallback (cmux only).
|
|
36
|
+
*/
|
|
37
|
+
group?: string;
|
|
13
38
|
}
|
|
14
39
|
export interface Multiplexer {
|
|
15
40
|
readonly kind: MultiplexerKind;
|
|
16
41
|
isActive(): boolean;
|
|
17
42
|
createWindow(opts: CreateWindowOpts): Promise<SessionResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Add a tab running `command` to an existing window/workspace named
|
|
45
|
+
* `windowName`. cmux relocates a live surface into that workspace so the two
|
|
46
|
+
* tabs (e.g. `work` + `fix-loop`) share one ticket workspace. Backends with
|
|
47
|
+
* no in-window tab concept (tmux, none) create a separate window named
|
|
48
|
+
* `<tabName>-<windowName>` instead. Falls back to creating the window when it
|
|
49
|
+
* doesn't exist yet.
|
|
50
|
+
*/
|
|
51
|
+
addTab(opts: AddTabOpts): Promise<SessionResult>;
|
|
18
52
|
selectWindow(name: string): Promise<SessionResult>;
|
|
19
53
|
sendCommand(name: string, command: string): SessionResult;
|
|
20
|
-
isSessionAlive(ticketId: string): boolean;
|
|
21
54
|
}
|
|
22
55
|
export declare function shellEscape(s: string): string;
|