pi-lazy-panel 0.1.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 +21 -0
- package/README.md +104 -0
- package/README.zh.md +104 -0
- package/docs/design.md +235 -0
- package/docs/keybindings.md +172 -0
- package/docs/keybindings.zh.md +172 -0
- package/package.json +67 -0
- package/src/actions/session-actions.ts +585 -0
- package/src/actions/tree-actions.ts +161 -0
- package/src/config/config.ts +155 -0
- package/src/config/keymap.ts +219 -0
- package/src/config/keys.ts +279 -0
- package/src/config/pi-settings.ts +54 -0
- package/src/constants.ts +48 -0
- package/src/data/changelog.ts +63 -0
- package/src/data/content.ts +178 -0
- package/src/data/search.ts +183 -0
- package/src/data/sessions.ts +148 -0
- package/src/data/tree-fold.ts +163 -0
- package/src/data/tree.ts +258 -0
- package/src/i18n/index.ts +103 -0
- package/src/i18n/locales/en.json +341 -0
- package/src/i18n/locales/zh.json +341 -0
- package/src/index.ts +155 -0
- package/src/types.ts +346 -0
- package/src/ui/app.ts +2892 -0
- package/src/ui/frame.ts +116 -0
- package/src/ui/mouse-input.ts +197 -0
- package/src/ui/mouse.ts +99 -0
- package/src/ui/panes/content-pane.ts +132 -0
- package/src/ui/panes/sessions-pane.ts +161 -0
- package/src/ui/panes/tree-pane.ts +152 -0
- package/src/ui/search-highlight.ts +108 -0
- package/src/ui/tree-lines.ts +111 -0
- package/src/ui/tree-outline.ts +80 -0
- package/src/ui/widgets/changelog-dialog.ts +196 -0
- package/src/ui/widgets/compact-dialog.ts +29 -0
- package/src/ui/widgets/confirm-dialog.ts +98 -0
- package/src/ui/widgets/export-dialog.ts +62 -0
- package/src/ui/widgets/footer.ts +117 -0
- package/src/ui/widgets/fork-dialog.ts +28 -0
- package/src/ui/widgets/help-overlay.ts +220 -0
- package/src/ui/widgets/import-dialog.ts +32 -0
- package/src/ui/widgets/input-dialog.ts +136 -0
- package/src/ui/widgets/label-dialog.ts +28 -0
- package/src/ui/widgets/new-session-dialog.ts +29 -0
- package/src/ui/widgets/prompt-bar.ts +92 -0
- package/src/ui/widgets/rename-dialog.ts +29 -0
- package/src/ui/widgets/restore-dialog.ts +60 -0
- package/src/ui/widgets/search-bar.ts +65 -0
- package/src/ui/widgets/select-dialog.ts +193 -0
- package/src/ui/widgets/session-info-dialog.ts +189 -0
- package/src/ui/widgets/tree-dialog.ts +293 -0
- package/src/utils/format.ts +83 -0
- package/src/utils/paths.ts +33 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-lazy-panel — entry point.
|
|
3
|
+
*
|
|
4
|
+
* Registers the `/lazy-panel` command. When invoked, it opens the full-screen
|
|
5
|
+
* lazygit-style panel (see ./ui/app.ts). Everything else lives in sub-modules:
|
|
6
|
+
*
|
|
7
|
+
* config/ keymap + user configuration
|
|
8
|
+
* data/ read-only adapters over pi's SessionManager (sessions, tree, content)
|
|
9
|
+
* actions/ side-effecting operations (resume, restore, label, copy, delete, fork, ...)
|
|
10
|
+
* ui/ TUI components (panes, dialogs, footer, search bar)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import {
|
|
18
|
+
cloneSession,
|
|
19
|
+
compactSession,
|
|
20
|
+
copyLastReply,
|
|
21
|
+
copyText,
|
|
22
|
+
deleteSession,
|
|
23
|
+
exportSession,
|
|
24
|
+
exportTarget,
|
|
25
|
+
forkSession,
|
|
26
|
+
importSession,
|
|
27
|
+
newSession,
|
|
28
|
+
renameSession,
|
|
29
|
+
resumeSession,
|
|
30
|
+
shareSession,
|
|
31
|
+
} from "./actions/session-actions.ts";
|
|
32
|
+
import { copyNodeText, labelNode, restoreNode } from "./actions/tree-actions.ts";
|
|
33
|
+
import { loadConfig } from "./config/config.ts";
|
|
34
|
+
import { loadPiSettings } from "./config/pi-settings.ts";
|
|
35
|
+
import { COMMAND_NAME } from "./constants.ts";
|
|
36
|
+
import { loadChangelog } from "./data/changelog.ts";
|
|
37
|
+
import { loadContent, loadForkPoints, loadSessionInfo } from "./data/content.ts";
|
|
38
|
+
import { initI18n, t } from "./i18n/index.ts";
|
|
39
|
+
import { listSessions, sortSessions } from "./data/sessions.ts";
|
|
40
|
+
import { applyTreeFilter, loadTree } from "./data/tree.ts";
|
|
41
|
+
import { type ActionSource, type DataSource, LazyPanel } from "./ui/app.ts";
|
|
42
|
+
import { attachMouse } from "./ui/mouse-input.ts";
|
|
43
|
+
|
|
44
|
+
/** 读本插件 package.json 的版本号,展示在 footer 右下角;读不到就返回空串(不显示)。 */
|
|
45
|
+
function extensionVersion(): string {
|
|
46
|
+
try {
|
|
47
|
+
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
48
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: unknown };
|
|
49
|
+
return typeof pkg.version === "string" ? pkg.version : "";
|
|
50
|
+
} catch {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function (pi: ExtensionAPI) {
|
|
56
|
+
// 按系统语言初始化 i18n(命令描述在注册时就要用到,一次会话内固定)。
|
|
57
|
+
initI18n();
|
|
58
|
+
pi.registerCommand(COMMAND_NAME, {
|
|
59
|
+
description: t("command.description"),
|
|
60
|
+
handler: async (_args, ctx) => {
|
|
61
|
+
// 按系统语言初始化 i18n(一次会话内固定),之后所有 UI 文案走 t()。
|
|
62
|
+
initI18n();
|
|
63
|
+
if (ctx.mode !== "tui") {
|
|
64
|
+
ctx.ui.notify(t("notify.tuiOnly", { command: COMMAND_NAME }), "warning");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const config = await loadConfig(getAgentDir());
|
|
68
|
+
// 用户配置里的 locale(en / zh)覆盖系统语言;没配就沿用上面按系统语言检测的结果。t() 在渲染时才调用,这里切换后面板 UI 即用新语言。
|
|
69
|
+
if (config.locale) initI18n(config.locale);
|
|
70
|
+
// pi 自己的 branchSummary.skipPrompt 打开时,TREE Enter 和内置 /tree 一样不弹摘要菜单;
|
|
71
|
+
// treeFilterMode 是内置 /tree 的默认过滤,面板的 TREE 也从它开始。
|
|
72
|
+
const piSettings = loadPiSettings(ctx.cwd, getAgentDir(), ctx.isProjectTrusted());
|
|
73
|
+
const data: DataSource = {
|
|
74
|
+
listSessions: async (scope, sort) => sortSessions(await listSessions({ cwd: ctx.cwd, scope }), sort),
|
|
75
|
+
loadTree: async (file, filter) => applyTreeFilter(await loadTree(file), filter),
|
|
76
|
+
loadContent: (file, leafEntryId) =>
|
|
77
|
+
loadContent(leafEntryId ? { sessionFile: file, leafEntryId } : { sessionFile: file }),
|
|
78
|
+
loadSessionInfo,
|
|
79
|
+
loadForkPoints,
|
|
80
|
+
loadChangelog: () => loadChangelog(),
|
|
81
|
+
};
|
|
82
|
+
// 副作用统一走 actions 层;目标是当前会话时用 pi 内存里的 API(setLabel / setSessionName / navigateTree),
|
|
83
|
+
// 其他历史会话则直接读写文件或先 switchSession。
|
|
84
|
+
const actions: ActionSource = {
|
|
85
|
+
copyNodeText,
|
|
86
|
+
setNodeLabel: (file, entryId, label) => labelNode(pi, ctx, file, entryId, label),
|
|
87
|
+
resumeSession: (file) => resumeSession(ctx, file),
|
|
88
|
+
restoreNode: (file, entryId, options) => restoreNode(ctx, file, entryId, options),
|
|
89
|
+
deleteSession: (file) => deleteSession(ctx, file),
|
|
90
|
+
renameSession: (file, name) => renameSession(pi, ctx, file, name),
|
|
91
|
+
newSession: (name) => newSession(ctx, name),
|
|
92
|
+
forkSession: (file, entryId) => forkSession(ctx, file, entryId),
|
|
93
|
+
cloneSession: (file) => cloneSession(ctx, file),
|
|
94
|
+
compactSession: (file, customInstructions) => compactSession(ctx, file, customInstructions),
|
|
95
|
+
copyLastReply,
|
|
96
|
+
copyText,
|
|
97
|
+
// 导出 / 分享的 HTML 走 pi 自己的 `pi --export`;相对路径和默认文件名都按 pi 的工作目录算。
|
|
98
|
+
exportTarget: (file, format, input) => exportTarget(ctx.cwd, file, format, input),
|
|
99
|
+
exportSession: (file, format, outputPath) => exportSession(ctx, file, format, outputPath),
|
|
100
|
+
importSession: (input) => importSession(ctx, input),
|
|
101
|
+
shareSession: (file) => shareSession(file),
|
|
102
|
+
};
|
|
103
|
+
// overlay 句柄在面板显示后才拿到;Enter 等待 pi 切换时用它暂时隐藏面板。
|
|
104
|
+
let setHidden: ((hidden: boolean) => void) | undefined;
|
|
105
|
+
// regular 模式下 pi 不开鼠标追踪,面板自己开;关闭时再关掉(fullscreen 由 pi 管,不碰)。
|
|
106
|
+
let disableMouse: (() => void) | undefined;
|
|
107
|
+
const currentFile = ctx.sessionManager.getSessionFile();
|
|
108
|
+
const version = extensionVersion();
|
|
109
|
+
|
|
110
|
+
await ctx.ui.custom<void>(
|
|
111
|
+
(tui, theme, _keybindings, done) => {
|
|
112
|
+
// overlay 模式默认不清理"腾空"的行(pi 的 terminal.clearOnShrink 默认 false):关掉 ? 帮助框后
|
|
113
|
+
// 会留残影(切面板时闪一下),在有内容的当前会话里滚动时整块面板还会整体往下漂。全屏面板下打开
|
|
114
|
+
// 这个开关,让缩小后的区域被清掉。老版本 pi 可能没有这个方法,先做一次存在性判断。
|
|
115
|
+
if (typeof tui.setClearOnShrink === "function") tui.setClearOnShrink(true);
|
|
116
|
+
const panel = new LazyPanel({
|
|
117
|
+
theme,
|
|
118
|
+
data,
|
|
119
|
+
actions,
|
|
120
|
+
getHeight: () => tui.terminal.rows,
|
|
121
|
+
requestRender: () => tui.requestRender(),
|
|
122
|
+
onClose: () => done(),
|
|
123
|
+
setHidden: (hidden) => setHidden?.(hidden),
|
|
124
|
+
keymap: config.keymap,
|
|
125
|
+
initialState: { scope: config.defaultScope, sort: config.defaultSort, treeFilter: piSettings.treeFilter },
|
|
126
|
+
leftColumnRatio: config.leftColumnRatio,
|
|
127
|
+
skipSummaryPrompt: piSettings.skipBranchSummaryPrompt,
|
|
128
|
+
// 打开时 SESSIONS 光标落到 pi 当前打开的会话上;新会话没有文件 / 还没列出时留在第一行。
|
|
129
|
+
...(currentFile ? { currentSessionFile: currentFile } : {}),
|
|
130
|
+
// 配置文件有问题时在底部提示,但不阻止面板打开。
|
|
131
|
+
...(config.warnings.length ? { status: config.warnings[0] } : {}),
|
|
132
|
+
// footer 右下角的版本号(读不到 package.json 时为空、不显示)。
|
|
133
|
+
...(version ? { version } : {}),
|
|
134
|
+
});
|
|
135
|
+
// fullscreen 模式 pi 已经开了鼠标并派给 overlay 的 handleMouse;regular 模式
|
|
136
|
+
// 由 attachMouse 自己开 SGR 上报并解析(fullscreen 下是 no-op)。
|
|
137
|
+
disableMouse = attachMouse(tui, panel);
|
|
138
|
+
void panel.load();
|
|
139
|
+
return panel;
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
// Full-screen overlay: covers pi's own header/editor/footer instead of
|
|
143
|
+
// being embedded in the editor slot (which would overflow the terminal).
|
|
144
|
+
overlay: true,
|
|
145
|
+
overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%", margin: 0 },
|
|
146
|
+
onHandle: (handle) => {
|
|
147
|
+
setHidden = (hidden) => handle.setHidden(hidden);
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
// 面板关闭后恢复终端自己的滚动 / 选择。
|
|
152
|
+
disableMouse?.();
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions for pi-lazy-panel.
|
|
3
|
+
*
|
|
4
|
+
* Keep this file free of runtime code — types only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { KEY_SCOPES, PANE_IDS, SESSION_SORT_MODES } from "./constants.ts";
|
|
8
|
+
import type { Locale } from "./i18n/index.ts";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Panes / focus
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/** One of the three focusable panes. */
|
|
15
|
+
export type PaneId = (typeof PANE_IDS)[number];
|
|
16
|
+
|
|
17
|
+
/** Which set of sessions the sessions pane lists. */
|
|
18
|
+
export type ListScope = "current-folder" | "all";
|
|
19
|
+
|
|
20
|
+
/** Sort order of the sessions pane (mirrors pi's /resume picker; `s` cycles them in `SESSION_SORT_MODES` order). */
|
|
21
|
+
export type SessionSortMode = (typeof SESSION_SORT_MODES)[number];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Input mode of the panel, shown in the footer (vim-like). `restore` = the
|
|
25
|
+
* summary menu / custom prompt of TREE Enter is open, `tree` = the full tree
|
|
26
|
+
* dialog (`a` in the tree pane) is open, `confirm` / `rename` / `info` = the
|
|
27
|
+
* delete confirmation, the Rename prompt or the Session Info dialog is open;
|
|
28
|
+
* `export` / `import` / `share` = the dialogs of SESSIONS `e` / `I` / `S`;
|
|
29
|
+
* `changelog` = the `@` changelog dialog.
|
|
30
|
+
*/
|
|
31
|
+
export type PanelMode =
|
|
32
|
+
| "normal"
|
|
33
|
+
| "search"
|
|
34
|
+
| "label"
|
|
35
|
+
| "restore"
|
|
36
|
+
| "tree"
|
|
37
|
+
| "confirm"
|
|
38
|
+
| "rename"
|
|
39
|
+
| "info"
|
|
40
|
+
| "new"
|
|
41
|
+
| "fork"
|
|
42
|
+
| "clone"
|
|
43
|
+
| "compact"
|
|
44
|
+
| "export"
|
|
45
|
+
| "import"
|
|
46
|
+
| "share"
|
|
47
|
+
| "changelog"
|
|
48
|
+
| "visual"
|
|
49
|
+
| "preview";
|
|
50
|
+
|
|
51
|
+
/** A `key description` pair shown as a hint in the footer or a prompt bar. */
|
|
52
|
+
export type KeyHint = [key: string, text: string];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What Enter did (SESSIONS resume / TREE restore):
|
|
56
|
+
* `switched` = pi now shows another session, `restored` = the leaf moved to the
|
|
57
|
+
* chosen node, `unchanged` = pi was already there, `compacted` = the current
|
|
58
|
+
* session's active branch was compacted in place. Failures throw instead.
|
|
59
|
+
*/
|
|
60
|
+
export type EnterOutcome = "switched" | "restored" | "unchanged" | "compacted";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How a session file was removed (SESSIONS `d`, same as pi's /resume):
|
|
64
|
+
* `trash` = moved to the system trash by the `trash` CLI, `unlink` = deleted
|
|
65
|
+
* for good because `trash` was unavailable or failed.
|
|
66
|
+
*/
|
|
67
|
+
export type DeleteMethod = "trash" | "unlink";
|
|
68
|
+
|
|
69
|
+
/** File format of SESSIONS `e` (pi's /export): the whole tree as HTML, or the active branch as JSONL. */
|
|
70
|
+
export type ExportFormat = "html" | "jsonl";
|
|
71
|
+
|
|
72
|
+
/** Where an export would be written, resolved from what the user typed (blank = pi's default name in the cwd). */
|
|
73
|
+
export interface ExportTarget {
|
|
74
|
+
/** Absolute output path. */
|
|
75
|
+
path: string;
|
|
76
|
+
/** A file is already there (the panel asks before overwriting it). */
|
|
77
|
+
exists: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** What SESSIONS `S` (pi's /share) produced: the pi.dev viewer link and the gist behind it. */
|
|
81
|
+
export interface ShareResult {
|
|
82
|
+
url: string;
|
|
83
|
+
gistUrl: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* How TREE Enter leaves the branch it abandons — the three choices of pi's
|
|
88
|
+
* `/tree`: no summary, a model-written summary of the abandoned branch, or a
|
|
89
|
+
* summary written with extra instructions. Passed through to `ctx.navigateTree`.
|
|
90
|
+
*/
|
|
91
|
+
export interface RestoreOptions {
|
|
92
|
+
summarize: boolean;
|
|
93
|
+
/** Extra instructions for the summarizer ("Summarize with custom prompt"). */
|
|
94
|
+
customInstructions?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Data rows
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/** A row in the sessions (left-top) pane. */
|
|
102
|
+
export interface SessionRow {
|
|
103
|
+
/** Absolute path to the session .jsonl file. */
|
|
104
|
+
file: string;
|
|
105
|
+
/** Session UUID. */
|
|
106
|
+
id: string;
|
|
107
|
+
/** User-defined display name, if any (/name). */
|
|
108
|
+
name?: string;
|
|
109
|
+
/** Working directory the session was created in. */
|
|
110
|
+
cwd: string;
|
|
111
|
+
/** Session file this one was forked from, if any (drives "threaded" sort). */
|
|
112
|
+
parentFile?: string;
|
|
113
|
+
/** Indentation level assigned by "threaded" sort (0 = root). */
|
|
114
|
+
threadDepth?: number;
|
|
115
|
+
/** Model id last used in the session, if known. */
|
|
116
|
+
model?: string;
|
|
117
|
+
/** Short preview of the first user message. */
|
|
118
|
+
preview: string;
|
|
119
|
+
createdAt: number;
|
|
120
|
+
updatedAt: number;
|
|
121
|
+
messageCount: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A row in the tree (left-bottom) pane. */
|
|
125
|
+
export interface TreeRow {
|
|
126
|
+
/** Entry id inside the session. */
|
|
127
|
+
entryId: string;
|
|
128
|
+
/**
|
|
129
|
+
* Nearest ancestor that is also a row, undefined for roots. Entries that
|
|
130
|
+
* never become rows (labels, hidden by the filter…) are skipped over, so the
|
|
131
|
+
* rows always form a consistent forest the UI can draw guide lines for.
|
|
132
|
+
*/
|
|
133
|
+
parentId?: string;
|
|
134
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
135
|
+
/**
|
|
136
|
+
* Coarse category used by the tree filters (same split as pi's /tree):
|
|
137
|
+
* `message` user / assistant, `tool` tool results, `system` structural
|
|
138
|
+
* entries always shown (system prompt, bash, compaction, branch summary),
|
|
139
|
+
* `meta` bookkeeping hidden by default (model / thinking / name changes).
|
|
140
|
+
*/
|
|
141
|
+
kind: "message" | "tool" | "system" | "meta";
|
|
142
|
+
/** User-defined label on this entry (/tree shift+t). */
|
|
143
|
+
label?: string;
|
|
144
|
+
/** Truncated single-line text. */
|
|
145
|
+
text: string;
|
|
146
|
+
timestamp: number;
|
|
147
|
+
/** Whether this entry is on the currently active branch. */
|
|
148
|
+
onActiveBranch: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Restoring here would leave the conversation where it already is (the leaf,
|
|
151
|
+
* or a message followed only by bookkeeping entries — see `isEffectiveLeaf`),
|
|
152
|
+
* so Enter skips the summary menu.
|
|
153
|
+
*/
|
|
154
|
+
isLeaf?: boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Tree filters (mirror /tree ctrl+d/t/u/l/a: `no-tools` is pi's ctrl+t,
|
|
159
|
+
* `labeled` pi's `labeled-only`). Set from the tree dialog (d/t/u/l/a); the
|
|
160
|
+
* pane shows the same filtered tree.
|
|
161
|
+
*/
|
|
162
|
+
export type TreeFilter = "default" | "no-tools" | "user-only" | "labeled" | "all";
|
|
163
|
+
|
|
164
|
+
/** A message block rendered in the content (right) pane. */
|
|
165
|
+
export interface ContentBlock {
|
|
166
|
+
entryId: string;
|
|
167
|
+
role: "user" | "assistant";
|
|
168
|
+
timestamp: number;
|
|
169
|
+
/** Raw markdown to render. */
|
|
170
|
+
markdown: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A user message the `/fork` selector can fork before (entry id + one-line preview). */
|
|
174
|
+
export interface ForkPoint {
|
|
175
|
+
entryId: string;
|
|
176
|
+
text: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Data shown in the Session Info dialog (mirrors /session). */
|
|
180
|
+
export interface SessionInfo {
|
|
181
|
+
name?: string;
|
|
182
|
+
model?: string;
|
|
183
|
+
messages: number;
|
|
184
|
+
tokens: number;
|
|
185
|
+
cost: number;
|
|
186
|
+
createdAt: number;
|
|
187
|
+
updatedAt: number;
|
|
188
|
+
path: string;
|
|
189
|
+
id: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Search
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Parsed search query. Supports GitHub-style qualifiers:
|
|
198
|
+
* name:foo model:opus path:bar tag:scan role:user after:2026-09-01 before:2026-09-20
|
|
199
|
+
* plus free text.
|
|
200
|
+
*/
|
|
201
|
+
export interface SearchQuery {
|
|
202
|
+
text: string;
|
|
203
|
+
name?: string;
|
|
204
|
+
model?: string;
|
|
205
|
+
path?: string;
|
|
206
|
+
tag?: string;
|
|
207
|
+
/** Tree rows: the role (`user`, `assistant`, `system`, `tool`) must contain it. */
|
|
208
|
+
role?: string;
|
|
209
|
+
after?: Date;
|
|
210
|
+
before?: Date;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The `/` search active in one pane (lazygit-style: the rows are not filtered,
|
|
215
|
+
* the cursor jumps between the matches with n / N). Kept per pane, so switching
|
|
216
|
+
* panes does not lose a query.
|
|
217
|
+
*
|
|
218
|
+
* 每个面板各自的搜索状态:列表不过滤,只在匹配之间跳转。
|
|
219
|
+
*/
|
|
220
|
+
export interface PaneSearch {
|
|
221
|
+
/** Raw query as typed after `/`. */
|
|
222
|
+
query: string;
|
|
223
|
+
/**
|
|
224
|
+
* Matches, ascending: row indices for the list panes (the sessions list; the
|
|
225
|
+
* whole tree, folded rows included) and body-line indices of the rendered
|
|
226
|
+
* layout for the content pane.
|
|
227
|
+
*/
|
|
228
|
+
matches: number[];
|
|
229
|
+
/** Position in `matches` of the current match (the content pane's scroll target), -1 when there is none. */
|
|
230
|
+
current: number;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* What a pane paints for the search active in it: which of the rows / lines it
|
|
235
|
+
* renders match, which one is the current match, the terms to highlight and
|
|
236
|
+
* the counts for its header ("2/7 matches").
|
|
237
|
+
*/
|
|
238
|
+
export interface SearchView {
|
|
239
|
+
/** Terms to highlight on matching rows (free-text tokens and qualifier values). */
|
|
240
|
+
terms: string[];
|
|
241
|
+
/** Indices, in what the pane renders, of the matching rows (list panes) or body lines (content pane). */
|
|
242
|
+
matches: ReadonlySet<number>;
|
|
243
|
+
/** Index of the current match, undefined when the cursor is not on one. */
|
|
244
|
+
current: number | undefined;
|
|
245
|
+
/** 1-based position of the current match among the matches, 0 when there is none. */
|
|
246
|
+
position: number;
|
|
247
|
+
total: number;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// Keymap
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
/** Logical actions the panel can perform. Keys are bound to these. */
|
|
255
|
+
export type ActionId =
|
|
256
|
+
// global
|
|
257
|
+
| "focus-next"
|
|
258
|
+
| "focus-prev"
|
|
259
|
+
| "focus-sessions"
|
|
260
|
+
| "focus-tree"
|
|
261
|
+
| "focus-content"
|
|
262
|
+
| "scope-current"
|
|
263
|
+
| "scope-all"
|
|
264
|
+
| "help"
|
|
265
|
+
| "quit"
|
|
266
|
+
| "search"
|
|
267
|
+
| "search-next"
|
|
268
|
+
| "search-prev"
|
|
269
|
+
| "changelog"
|
|
270
|
+
// list navigation
|
|
271
|
+
| "move-down"
|
|
272
|
+
| "move-up"
|
|
273
|
+
| "go-top"
|
|
274
|
+
| "go-bottom"
|
|
275
|
+
| "scroll-content-down"
|
|
276
|
+
| "scroll-content-up"
|
|
277
|
+
// sessions pane
|
|
278
|
+
| "session-resume"
|
|
279
|
+
| "session-delete"
|
|
280
|
+
| "session-rename"
|
|
281
|
+
| "session-fork"
|
|
282
|
+
| "session-toggle-select"
|
|
283
|
+
| "session-export"
|
|
284
|
+
| "session-import"
|
|
285
|
+
| "session-share"
|
|
286
|
+
| "session-clone"
|
|
287
|
+
| "session-copy-last-reply"
|
|
288
|
+
| "session-compact"
|
|
289
|
+
| "session-sort"
|
|
290
|
+
| "session-new"
|
|
291
|
+
| "session-info"
|
|
292
|
+
// tree pane
|
|
293
|
+
| "tree-restore"
|
|
294
|
+
| "tree-copy"
|
|
295
|
+
| "tree-label"
|
|
296
|
+
| "tree-open"
|
|
297
|
+
| "tree-fold"
|
|
298
|
+
// tree dialog only (the dialog also uses the tree pane's actions and the global `search`)
|
|
299
|
+
| "tree-filter-default"
|
|
300
|
+
| "tree-filter-no-tools"
|
|
301
|
+
| "tree-filter-user"
|
|
302
|
+
| "tree-filter-labeled"
|
|
303
|
+
| "tree-filter-all"
|
|
304
|
+
| "tree-dialog-close";
|
|
305
|
+
// content pane is read-only and only uses the shared navigation actions
|
|
306
|
+
// (move-down / move-up / go-top / go-bottom) plus the global search, see docs/design.md.
|
|
307
|
+
// Tree filters (d/t/u/l/a) live in the tree dialog (`tree-open`), which also has
|
|
308
|
+
// its own live-filtering search row; `/` in the panes jumps between matches.
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* A single key chord in pi-tui key syntax, e.g. "j", "ctrl+d", "tab".
|
|
312
|
+
* Multi-key sequences are written either verbatim when every step is one
|
|
313
|
+
* printable character ("gg", "yy") or space-separated ("ctrl+w h").
|
|
314
|
+
* An uppercase letter ("G") means shift + that letter.
|
|
315
|
+
*/
|
|
316
|
+
export type KeyChord = string;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Binding scope: the global scope, one pane, or the tree dialog. Keys are
|
|
320
|
+
* resolved innermost first: `tree-dialog` → `tree` → `global` inside the
|
|
321
|
+
* dialog, `<pane>` → `global` in a pane (see `scopeChain` in config/keys.ts).
|
|
322
|
+
*/
|
|
323
|
+
export type KeyScope = (typeof KEY_SCOPES)[number];
|
|
324
|
+
|
|
325
|
+
/** Keymap for one scope (a pane, the tree dialog or the global scope). */
|
|
326
|
+
export type PaneKeymap = Partial<Record<ActionId, KeyChord | KeyChord[]>>;
|
|
327
|
+
|
|
328
|
+
/** Full keymap: global bindings plus per-scope overrides. */
|
|
329
|
+
export type Keymap = Record<KeyScope, PaneKeymap>;
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// User config
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
/** Per-scope keymap in the user file. `null` unbinds an action. */
|
|
336
|
+
export type UserPaneKeymap = Partial<Record<ActionId, KeyChord | KeyChord[] | null>>;
|
|
337
|
+
|
|
338
|
+
/** Shape of `~/.pi/agent/lazy-panel.json`. All fields optional. */
|
|
339
|
+
export interface UserConfig {
|
|
340
|
+
keymap?: Partial<Record<KeyScope, UserPaneKeymap>>;
|
|
341
|
+
/** UI 语言("en" / "zh");缺省时按系统语言自动检测。 */
|
|
342
|
+
locale?: Locale;
|
|
343
|
+
defaultScope?: ListScope;
|
|
344
|
+
defaultSort?: SessionSortMode;
|
|
345
|
+
leftColumnRatio?: number;
|
|
346
|
+
}
|