agy-powerline 1.0.2 → 1.1.1
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/CHANGELOG.md +19 -0
- package/README.md +4 -1
- package/package.json +1 -1
- package/src/cli/bundle.js +82 -0
- package/src/cli/init.js +24 -9
- package/src/lib/catalog.js +18 -0
- package/src/lib/config.js +7 -1
- package/src/lib/fit.js +3 -0
- package/src/lib/formatters.js +34 -0
- package/src/lib/git.js +70 -2
- package/src/lib/layouts/lines.js +29 -0
- package/src/lib/layouts/pills.js +28 -0
- package/src/lib/log.js +24 -1
- package/src/lib/mock-data.js +7 -0
- package/src/lib/paths.js +6 -2
- package/src/lib/payload-dump.js +18 -0
- package/src/lib/session.js +10 -0
- package/src/lib/widget-style.js +7 -0
- package/src/runtime/statusline.js +80 -46
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.1] - 2026-10-12
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- The statusline is installed as a single `statusline.js` instead of about 16 files, which cuts agy-powerline's own share of each refresh by about a third on Windows; run `npx agy-powerline init --yes` to switch, and `init` removes the old files (#23)
|
|
13
|
+
- The ahead/behind counts are kept per pair of commits, so a branch that is ahead of or behind its upstream no longer starts `git` on every refresh (about 60 ms each on Windows) (#24)
|
|
14
|
+
|
|
15
|
+
## [1.1.0] - 2026-10-12
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `tasks` widget: running subagents and background tasks, e.g. `👥 2 subagents, 1 task`, hidden when nothing runs; part of the `full` preset (#14)
|
|
20
|
+
- `mode` widget: the approval mode (e.g. `accept-edits`) and the Vim mode, which Antigravity's own status line shows; hidden in the default mode with Vim editing off; part of the `workspace` and `full` presets (#15)
|
|
21
|
+
- `title` widget: the conversation's title on one line, cut to 30 columns, hidden until the conversation has one (#16)
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- The statusline no longer hangs until Antigravity kills it when Antigravity keeps its input open without sending anything; repeated kills made Antigravity turn the statusline off (#13)
|
|
26
|
+
|
|
8
27
|
## [1.0.2] - 2026-10-11
|
|
9
28
|
|
|
10
29
|
### Security
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ Start from a preset, then adjust:
|
|
|
42
42
|
| ----------- | ---------------------------------------------------------------------------- |
|
|
43
43
|
| `default` | Model, context, git; 5h and 7d quotas with resets on a second row |
|
|
44
44
|
| `compact` | One line: short model, tightest quota, context %, git |
|
|
45
|
-
| `workspace` | Project, git, agent state, model; context
|
|
45
|
+
| `workspace` | Project, git, agent state, model; context, tightest quota and mode |
|
|
46
46
|
| `limits` | Every quota (5h, 7d, third-party), one per row |
|
|
47
47
|
| `full` | Everything: session, project and git; context, tokens and cache; both quotas |
|
|
48
48
|
|
|
@@ -86,6 +86,9 @@ Preview them all with `npx agy-powerline theme --gallery`.
|
|
|
86
86
|
| `git` | Branch, uncommitted changes and ahead/behind |
|
|
87
87
|
| `project`, `cwd` | Project folder, current folder |
|
|
88
88
|
| `agentState`, `plan`, `sandbox`, `artifacts`, `version` | What the agent is doing, your plan, sandbox, artifacts, CLI version |
|
|
89
|
+
| `tasks` | Running subagents and background tasks |
|
|
90
|
+
| `mode` | Approval mode (e.g. accept-edits) and Vim mode |
|
|
91
|
+
| `title` | The conversation's title |
|
|
89
92
|
| `totalTokens`, `cacheHit`, `cacheRead`, `cacheCreate` | Session tokens and prompt cache usage |
|
|
90
93
|
| `ram` | Memory used on your machine |
|
|
91
94
|
| `customText`, `exec` | Your own text, or the output of a [shell command](docs/configuration.md#shell-command-widget-exec) |
|
package/package.json
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Builds the installed statusline.js: the runtime script and the library
|
|
4
|
+
// files in RUNTIME_FILES, joined into one file. Node loads a single file much
|
|
5
|
+
// faster than ~16 small ones (on Windows, opening each file costs more than
|
|
6
|
+
// running its code), and Antigravity starts the script on every refresh.
|
|
7
|
+
//
|
|
8
|
+
// Each file keeps its own scope: its source is wrapped in a function that
|
|
9
|
+
// gets its own module, exports and require, the way Node wraps a file. No
|
|
10
|
+
// eval, no code generation beyond that wrapper. LINES records where each file
|
|
11
|
+
// starts, so the problem log can still name the original file and line.
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
const { RUNTIME_FILES } = require("../lib/paths");
|
|
16
|
+
|
|
17
|
+
const LIB_DIR = path.join(__dirname, "..", "lib");
|
|
18
|
+
const ENTRY = path.join(__dirname, "..", "runtime", "statusline.js");
|
|
19
|
+
|
|
20
|
+
// Requires between bundled files resolve inside the bundle; anything else
|
|
21
|
+
// (Node built-ins) goes to Node's own require.
|
|
22
|
+
const PRELUDE = `const __files = {};
|
|
23
|
+
const __cache = {};
|
|
24
|
+
function __require(from, id) {
|
|
25
|
+
if (!id.startsWith(".")) {
|
|
26
|
+
return require(id);
|
|
27
|
+
}
|
|
28
|
+
let key = require("path").posix.join(require("path").posix.dirname(from), id);
|
|
29
|
+
if (!key.endsWith(".js")) {
|
|
30
|
+
key += ".js";
|
|
31
|
+
}
|
|
32
|
+
if (!__files[key]) {
|
|
33
|
+
const err = new Error(\`Cannot find module '\${id}' from '\${from}'\`);
|
|
34
|
+
err.code = "MODULE_NOT_FOUND";
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
if (!__cache[key]) {
|
|
38
|
+
const module = { exports: {} };
|
|
39
|
+
__cache[key] = module;
|
|
40
|
+
__files[key](module, module.exports, (next) => __require(key, next));
|
|
41
|
+
}
|
|
42
|
+
return __cache[key].exports;
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
function toKey(rel) {
|
|
47
|
+
return rel.split(path.sep).join("/");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function wrapperLine(key) {
|
|
51
|
+
return `__files[${JSON.stringify(key)}] = function (module, exports, require) {`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildRuntimeBundle() {
|
|
55
|
+
const files = [
|
|
56
|
+
...RUNTIME_FILES.map((rel) => [toKey(rel), path.join(LIB_DIR, rel)]),
|
|
57
|
+
["statusline.js", ENTRY],
|
|
58
|
+
];
|
|
59
|
+
const parts = ["#!/usr/bin/env node", PRELUDE];
|
|
60
|
+
|
|
61
|
+
for (const [key, file] of files) {
|
|
62
|
+
const body = fs
|
|
63
|
+
.readFileSync(file, "utf8")
|
|
64
|
+
.replace(/^#!.*\n/, "")
|
|
65
|
+
.replace(/\n+$/, "");
|
|
66
|
+
parts.push(wrapperLine(key), body, "};");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// [first line of the file in the bundle, file name]; 1-based like stack traces.
|
|
70
|
+
const text = parts.join("\n");
|
|
71
|
+
const bundleLines = text.split("\n");
|
|
72
|
+
const lines = files.map(([key]) => [bundleLines.indexOf(wrapperLine(key)) + 2, key]);
|
|
73
|
+
|
|
74
|
+
return [
|
|
75
|
+
text,
|
|
76
|
+
`__require("statusline.js", "./log").setBundleLines(${JSON.stringify(lines)});`,
|
|
77
|
+
`__require("statusline.js", "./statusline");`,
|
|
78
|
+
"",
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { buildRuntimeBundle };
|
package/src/cli/init.js
CHANGED
|
@@ -18,9 +18,8 @@ const {
|
|
|
18
18
|
cmdBreakingMessage,
|
|
19
19
|
} = require("../lib/paths");
|
|
20
20
|
const { createRl, confirm } = require("../lib/prompt");
|
|
21
|
+
const { buildRuntimeBundle } = require("./bundle");
|
|
21
22
|
|
|
22
|
-
const RUNTIME_DIR = path.join(__dirname, "..", "runtime");
|
|
23
|
-
const LIB_DIR = path.join(__dirname, "..", "lib");
|
|
24
23
|
const DIM = "\x1b[2m";
|
|
25
24
|
const BOLD = "\x1b[1m";
|
|
26
25
|
const CYAN = "\x1b[36m";
|
|
@@ -29,6 +28,27 @@ const YELLOW = "\x1b[33m";
|
|
|
29
28
|
const RED = "\x1b[31m";
|
|
30
29
|
const RESET = "\x1b[0m";
|
|
31
30
|
|
|
31
|
+
// Up to 1.1.0 the runtime was installed as separate files next to
|
|
32
|
+
// statusline.js. They are part of the one-file statusline.js now; delete the
|
|
33
|
+
// old copies by name (regular files only), then the emptied layouts folder.
|
|
34
|
+
function removeSeparateRuntimeFiles(installDir) {
|
|
35
|
+
for (const rel of RUNTIME_FILES) {
|
|
36
|
+
const file = path.join(installDir, rel);
|
|
37
|
+
try {
|
|
38
|
+
if (fs.lstatSync(file).isFile()) {
|
|
39
|
+
fs.unlinkSync(file);
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Not there: nothing to remove.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
fs.rmdirSync(path.join(installDir, "layouts"));
|
|
47
|
+
} catch {
|
|
48
|
+
// Missing, or holds something that isn't ours: leave it.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
function assertUnixNode() {
|
|
33
53
|
if (!isWindowsNodeOnWsl()) {
|
|
34
54
|
return;
|
|
@@ -104,13 +124,8 @@ async function initCommand(options = {}) {
|
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
if (writeScripts) {
|
|
107
|
-
fs.
|
|
108
|
-
|
|
109
|
-
const src = path.join(LIB_DIR, rel);
|
|
110
|
-
const dest = path.join(installDir, rel);
|
|
111
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
112
|
-
fs.copyFileSync(src, dest);
|
|
113
|
-
}
|
|
127
|
+
fs.writeFileSync(destStatusline, buildRuntimeBundle(), "utf8");
|
|
128
|
+
removeSeparateRuntimeFiles(installDir);
|
|
114
129
|
|
|
115
130
|
if (process.platform !== "win32") {
|
|
116
131
|
try {
|
package/src/lib/catalog.js
CHANGED
|
@@ -105,6 +105,24 @@ const WIDGET_CATALOG = [
|
|
|
105
105
|
unique: true,
|
|
106
106
|
description: "number of artifacts",
|
|
107
107
|
},
|
|
108
|
+
{
|
|
109
|
+
id: "tasks",
|
|
110
|
+
label: "Subagents & Tasks",
|
|
111
|
+
unique: true,
|
|
112
|
+
description: "running subagents and background tasks, shown only when there are some",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "mode",
|
|
116
|
+
label: "Mode",
|
|
117
|
+
unique: true,
|
|
118
|
+
description: "approval mode (e.g. accept-edits) and Vim mode, shown only when one is on",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "title",
|
|
122
|
+
label: "Conversation Title",
|
|
123
|
+
unique: true,
|
|
124
|
+
description: "name of the current conversation, shown once it has one",
|
|
125
|
+
},
|
|
108
126
|
{ id: "ram", label: "RAM Usage", unique: true, description: "memory used on this machine" },
|
|
109
127
|
{ id: "version", label: "CLI Version", unique: true, description: "Antigravity CLI version" },
|
|
110
128
|
{
|
package/src/lib/config.js
CHANGED
|
@@ -40,6 +40,9 @@ const VALID_WIDGET_IDS = new Set([
|
|
|
40
40
|
"plan",
|
|
41
41
|
"agentState",
|
|
42
42
|
"artifacts",
|
|
43
|
+
"tasks",
|
|
44
|
+
"mode",
|
|
45
|
+
"title",
|
|
43
46
|
"ram",
|
|
44
47
|
"version",
|
|
45
48
|
"sandbox",
|
|
@@ -260,7 +263,7 @@ const PRESETS = {
|
|
|
260
263
|
workspace: {
|
|
261
264
|
id: "workspace",
|
|
262
265
|
name: "Several repos",
|
|
263
|
-
description: "Project, git, agent state and model; context
|
|
266
|
+
description: "Project, git, agent state and model; context, tightest quota and mode below",
|
|
264
267
|
widgets: [
|
|
265
268
|
{ id: "project" },
|
|
266
269
|
{ id: "git" },
|
|
@@ -271,6 +274,7 @@ const PRESETS = {
|
|
|
271
274
|
{ id: "context" },
|
|
272
275
|
{ id: "separator" },
|
|
273
276
|
{ id: "quota" },
|
|
277
|
+
{ id: "mode" },
|
|
274
278
|
],
|
|
275
279
|
},
|
|
276
280
|
limits: {
|
|
@@ -294,6 +298,7 @@ const PRESETS = {
|
|
|
294
298
|
description: "Session, project and git; context, tokens and cache; both quotas, on three rows",
|
|
295
299
|
widgets: [
|
|
296
300
|
{ id: "agentState" },
|
|
301
|
+
{ id: "tasks" },
|
|
297
302
|
{ id: "model" },
|
|
298
303
|
{ id: "plan" },
|
|
299
304
|
{ id: "sandbox" },
|
|
@@ -304,6 +309,7 @@ const PRESETS = {
|
|
|
304
309
|
{ id: "context" },
|
|
305
310
|
{ id: "totalTokens", split: true },
|
|
306
311
|
{ id: "cacheHit" },
|
|
312
|
+
{ id: "mode" },
|
|
307
313
|
{ id: "lineBreak" },
|
|
308
314
|
{ id: "fiveHourBar" },
|
|
309
315
|
{ id: "fiveHourReset" },
|
package/src/lib/fit.js
CHANGED
|
@@ -6,18 +6,21 @@ const { charWidth, visibleWidth } = require("./formatters");
|
|
|
6
6
|
// first entry first. Ids not listed (custom ones) go before all of them.
|
|
7
7
|
const DROP_ORDER = [
|
|
8
8
|
"customText",
|
|
9
|
+
"title",
|
|
9
10
|
"version",
|
|
10
11
|
"cacheCreate",
|
|
11
12
|
"cacheRead",
|
|
12
13
|
"totalTokens",
|
|
13
14
|
"cacheHit",
|
|
14
15
|
"artifacts",
|
|
16
|
+
"tasks",
|
|
15
17
|
"plan",
|
|
16
18
|
"ram",
|
|
17
19
|
"project",
|
|
18
20
|
"cwd",
|
|
19
21
|
"exec",
|
|
20
22
|
"agentState",
|
|
23
|
+
"mode",
|
|
21
24
|
"thirdPartyWeekly",
|
|
22
25
|
"thirdPartyFiveHour",
|
|
23
26
|
"weeklyReset",
|
package/src/lib/formatters.js
CHANGED
|
@@ -477,6 +477,37 @@ function formatGitSync(ahead, behind, ascii = false) {
|
|
|
477
477
|
return "";
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
+
// "2 subagents, 1 task", or "" when nothing runs.
|
|
481
|
+
function formatTasks(subagents, tasks) {
|
|
482
|
+
const part = (n, word) => (n > 0 ? `${n} ${word}${n === 1 ? "" : "s"}` : "");
|
|
483
|
+
return [part(subagents, "subagent"), part(tasks, "task")].filter(Boolean).join(", ");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// "accept-edits, NORMAL", or "" in the default mode with Vim editing off.
|
|
487
|
+
function formatMode(approval, vim) {
|
|
488
|
+
return [approval === "default" ? "" : approval, vim].filter(Boolean).join(", ");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Cuts text to `max` terminal columns, ending with an ellipsis when cut.
|
|
492
|
+
function clipText(text, max, ascii = false) {
|
|
493
|
+
const value = String(text);
|
|
494
|
+
if (visibleWidth(value) <= max) {
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
const ellipsis = ascii ? "..." : "…";
|
|
498
|
+
let out = "";
|
|
499
|
+
let width = 0;
|
|
500
|
+
for (const char of value) {
|
|
501
|
+
const w = charWidth(char.codePointAt(0));
|
|
502
|
+
if (width + w > max - ellipsis.length) {
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
out += char;
|
|
506
|
+
width += w;
|
|
507
|
+
}
|
|
508
|
+
return out.trimEnd() + ellipsis;
|
|
509
|
+
}
|
|
510
|
+
|
|
480
511
|
// Removes escape sequences and control characters from text that did not
|
|
481
512
|
// come from us (payload fields, branch names, command output), so it cannot
|
|
482
513
|
// drive the terminal: cursor moves, OSC 52 clipboard writes, hyperlinks...
|
|
@@ -555,6 +586,9 @@ module.exports = {
|
|
|
555
586
|
shortModelName,
|
|
556
587
|
formatTokenSplit,
|
|
557
588
|
formatGitSync,
|
|
589
|
+
formatTasks,
|
|
590
|
+
formatMode,
|
|
591
|
+
clipText,
|
|
558
592
|
quotaColor,
|
|
559
593
|
contextColor,
|
|
560
594
|
resolveQuotaThresholds,
|
package/src/lib/git.js
CHANGED
|
@@ -4,6 +4,14 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { logWarning } = require("./log");
|
|
6
6
|
const { firstText } = require("./formatters");
|
|
7
|
+
const { writeFileAtomic, PRIVATE_FILE_MODE } = require("./paths");
|
|
8
|
+
|
|
9
|
+
// Ahead/behind counts depend only on the two commits compared, so they are
|
|
10
|
+
// kept per commit pair: git runs again only when the branch or its upstream
|
|
11
|
+
// moves. Starting git costs ~60 ms on Windows, on every refresh otherwise.
|
|
12
|
+
const SYNC_CACHE_NAME = ".git-sync-cache.json";
|
|
13
|
+
const SYNC_CACHE_MAX = 20;
|
|
14
|
+
const COMMIT = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
7
15
|
|
|
8
16
|
// Only an absolute folder that exists can belong to the user's session. A
|
|
9
17
|
// relative path (a Windows path read on Linux, for one) resolves against
|
|
@@ -217,6 +225,54 @@ function upstreamRef(commonDir, branch) {
|
|
|
217
225
|
return remote === "." ? merge : `refs/remotes/${remote}/${tracked}`;
|
|
218
226
|
}
|
|
219
227
|
|
|
228
|
+
// Only next to a real install's config (like the exec cache); CLI previews
|
|
229
|
+
// and other copies don't write it.
|
|
230
|
+
function syncCachePath() {
|
|
231
|
+
try {
|
|
232
|
+
const { getConfigDir } = require("./config");
|
|
233
|
+
const dir = getConfigDir();
|
|
234
|
+
return dir && fs.existsSync(path.join(dir, "statusline.config.json"))
|
|
235
|
+
? path.join(dir, SYNC_CACHE_NAME)
|
|
236
|
+
: null;
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function readSyncCache(file) {
|
|
243
|
+
try {
|
|
244
|
+
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
245
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
246
|
+
} catch {
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function cachedSync(file, key) {
|
|
252
|
+
const value = file ? readSyncCache(file)[key] : null;
|
|
253
|
+
const valid =
|
|
254
|
+
Array.isArray(value) &&
|
|
255
|
+
value.length === 2 &&
|
|
256
|
+
value.every((n) => Number.isInteger(n) && n >= 0 && n <= 1e7);
|
|
257
|
+
return valid ? { ahead: value[0], behind: value[1] } : null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function saveSync(file, key, sync) {
|
|
261
|
+
if (!file) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
// Newest last; the oldest pairs go once there are more than SYNC_CACHE_MAX.
|
|
266
|
+
const cache = readSyncCache(file);
|
|
267
|
+
delete cache[key];
|
|
268
|
+
cache[key] = [sync.ahead, sync.behind];
|
|
269
|
+
const kept = Object.fromEntries(Object.entries(cache).slice(-SYNC_CACHE_MAX));
|
|
270
|
+
writeFileAtomic(file, JSON.stringify(kept), { mode: PRIVATE_FILE_MODE });
|
|
271
|
+
} catch {
|
|
272
|
+
// Best effort: the next refresh runs git again.
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
220
276
|
function getGitSync(gitDir, branch) {
|
|
221
277
|
if (!gitDir || !isSafeBranchName(branch)) {
|
|
222
278
|
return { ahead: 0, behind: 0 };
|
|
@@ -232,10 +288,19 @@ function getGitSync(gitDir, branch) {
|
|
|
232
288
|
return { ahead: 0, behind: 0 };
|
|
233
289
|
}
|
|
234
290
|
|
|
235
|
-
|
|
291
|
+
const upstreamHash = readRef(commonDir, upstream);
|
|
292
|
+
if (upstreamHash === localHash) {
|
|
236
293
|
return { ahead: 0, behind: 0 };
|
|
237
294
|
}
|
|
238
295
|
|
|
296
|
+
const key =
|
|
297
|
+
COMMIT.test(localHash) && COMMIT.test(upstreamHash) ? `${localHash}..${upstreamHash}` : "";
|
|
298
|
+
const cacheFile = key ? syncCachePath() : null;
|
|
299
|
+
const cached = cachedSync(cacheFile, key);
|
|
300
|
+
if (cached) {
|
|
301
|
+
return cached;
|
|
302
|
+
}
|
|
303
|
+
|
|
239
304
|
const { execFileSync } = require("child_process");
|
|
240
305
|
const out = execFileSync(
|
|
241
306
|
"git",
|
|
@@ -253,10 +318,12 @@ function getGitSync(gitDir, branch) {
|
|
|
253
318
|
|
|
254
319
|
const parts = out.split(/\s+/);
|
|
255
320
|
if (parts.length >= 2) {
|
|
256
|
-
|
|
321
|
+
const sync = {
|
|
257
322
|
ahead: parseInt(parts[0], 10) || 0,
|
|
258
323
|
behind: parseInt(parts[1], 10) || 0,
|
|
259
324
|
};
|
|
325
|
+
saveSync(cacheFile, key, sync);
|
|
326
|
+
return sync;
|
|
260
327
|
}
|
|
261
328
|
} catch (err) {
|
|
262
329
|
// A timeout or missing git is worth knowing about when ahead/behind
|
|
@@ -327,4 +394,5 @@ module.exports = {
|
|
|
327
394
|
gitHasDirtyMarkers,
|
|
328
395
|
vcsFromPayload,
|
|
329
396
|
resolveGit,
|
|
397
|
+
SYNC_CACHE_NAME,
|
|
330
398
|
};
|
package/src/lib/layouts/lines.js
CHANGED
|
@@ -8,6 +8,9 @@ const {
|
|
|
8
8
|
paceIndex,
|
|
9
9
|
paceGlyph,
|
|
10
10
|
formatGitSync,
|
|
11
|
+
formatTasks,
|
|
12
|
+
formatMode,
|
|
13
|
+
clipText,
|
|
11
14
|
formatTokens,
|
|
12
15
|
formatContextTokens,
|
|
13
16
|
shortModelName,
|
|
@@ -28,6 +31,7 @@ const {
|
|
|
28
31
|
hasNoData,
|
|
29
32
|
RAM_CAUTION,
|
|
30
33
|
RAM_DANGER,
|
|
34
|
+
TITLE_MAX,
|
|
31
35
|
} = require("../widget-style");
|
|
32
36
|
|
|
33
37
|
const LINES = NAMED_COLORS;
|
|
@@ -374,6 +378,31 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
|
|
|
374
378
|
}
|
|
375
379
|
return fg(palette.text) + tag() + String(session.artifacts) + RESET;
|
|
376
380
|
|
|
381
|
+
case "tasks": {
|
|
382
|
+
const text = formatTasks(session.subagents, session.tasks);
|
|
383
|
+
if (!text) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
return fg(palette.text) + tag() + text + RESET;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
case "mode": {
|
|
390
|
+
const text = formatMode(session.approvalMode, session.vimMode);
|
|
391
|
+
if (!text) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
// An approval mode like accept-edits changes what the agent may do alone.
|
|
395
|
+
const approval = session.approvalMode && session.approvalMode !== "default";
|
|
396
|
+
const color = approval ? palette.quotaHigh || palette.yellow || LINES.yellow : palette.text;
|
|
397
|
+
return fg(color) + tag() + text + RESET;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
case "title":
|
|
401
|
+
if (!session.title) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
return fg(palette.text) + tag() + clipText(session.title, TITLE_MAX, ascii) + RESET;
|
|
405
|
+
|
|
377
406
|
case "sandbox": {
|
|
378
407
|
if (!session.sandbox) {
|
|
379
408
|
return null;
|
package/src/lib/layouts/pills.js
CHANGED
|
@@ -24,6 +24,9 @@ const {
|
|
|
24
24
|
shortModelName,
|
|
25
25
|
formatTokenSplit,
|
|
26
26
|
formatGitSync,
|
|
27
|
+
formatTasks,
|
|
28
|
+
formatMode,
|
|
29
|
+
clipText,
|
|
27
30
|
homePath,
|
|
28
31
|
basenameOf,
|
|
29
32
|
label,
|
|
@@ -41,6 +44,7 @@ const {
|
|
|
41
44
|
hasNoData,
|
|
42
45
|
RAM_CAUTION,
|
|
43
46
|
RAM_DANGER,
|
|
47
|
+
TITLE_MAX,
|
|
44
48
|
} = require("../widget-style");
|
|
45
49
|
|
|
46
50
|
const WHITE = [255, 255, 255];
|
|
@@ -420,6 +424,30 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
|
|
|
420
424
|
}
|
|
421
425
|
return pill(colors.meta, lbl(String(session.artifacts)));
|
|
422
426
|
|
|
427
|
+
case "tasks": {
|
|
428
|
+
const text = formatTasks(session.subagents, session.tasks);
|
|
429
|
+
if (!text) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
return pill(colors.meta, lbl(text));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
case "mode": {
|
|
436
|
+
const text = formatMode(session.approvalMode, session.vimMode);
|
|
437
|
+
if (!text) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
// An approval mode like accept-edits changes what the agent may do alone.
|
|
441
|
+
const approval = session.approvalMode && session.approvalMode !== "default";
|
|
442
|
+
return pill(approval ? colors.caution : colors.meta, lbl(text));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
case "title":
|
|
446
|
+
if (!session.title) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
return pill(colors.meta, lbl(clipText(session.title, TITLE_MAX, ascii)));
|
|
450
|
+
|
|
423
451
|
case "ram": {
|
|
424
452
|
const pct = session.ramPct ?? 0;
|
|
425
453
|
let color = colors.meta;
|
package/src/lib/log.js
CHANGED
|
@@ -28,6 +28,28 @@ const IN_REPO = path.basename(__dirname) === "lib";
|
|
|
28
28
|
let logDir = null;
|
|
29
29
|
let context = {};
|
|
30
30
|
|
|
31
|
+
// The installed statusline.js is the runtime files joined into one (see
|
|
32
|
+
// src/cli/bundle.js). [first line, file name] for each of them, so a stack
|
|
33
|
+
// frame can still name the original file and line.
|
|
34
|
+
let bundleLines = null;
|
|
35
|
+
|
|
36
|
+
function setBundleLines(lines) {
|
|
37
|
+
bundleLines = Array.isArray(lines) ? lines : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function originalFrame(rel, line) {
|
|
41
|
+
if (!bundleLines || rel !== "statusline.js") {
|
|
42
|
+
return `${rel}:${line}`;
|
|
43
|
+
}
|
|
44
|
+
let match = null;
|
|
45
|
+
for (const entry of bundleLines) {
|
|
46
|
+
if (entry[0] <= line) {
|
|
47
|
+
match = entry;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return match ? `${match[1]}:${line - match[0] + 1}` : `${rel}:${line}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
// The copied runtime has no package.json; init records the version in
|
|
32
54
|
// install.json instead.
|
|
33
55
|
function agyPowerlineVersion() {
|
|
@@ -139,7 +161,7 @@ function ourFrames(stack) {
|
|
|
139
161
|
continue;
|
|
140
162
|
}
|
|
141
163
|
const fn = match[1] && /^[\w$.<> ]{1,60}$/.test(match[1]) ? `${match[1]} ` : "";
|
|
142
|
-
frames.push(`${fn}${rel.split(path.sep).join("/")
|
|
164
|
+
frames.push(`${fn}${originalFrame(rel.split(path.sep).join("/"), Number(match[3]))}`);
|
|
143
165
|
if (frames.length >= 8) {
|
|
144
166
|
break;
|
|
145
167
|
}
|
|
@@ -283,4 +305,5 @@ module.exports = {
|
|
|
283
305
|
logError,
|
|
284
306
|
logWarning,
|
|
285
307
|
clearLog,
|
|
308
|
+
setBundleLines,
|
|
286
309
|
};
|
package/src/lib/mock-data.js
CHANGED
|
@@ -62,6 +62,13 @@ function sampleData() {
|
|
|
62
62
|
},
|
|
63
63
|
sandbox: { enabled: false },
|
|
64
64
|
artifact_count: 2,
|
|
65
|
+
subagents: [
|
|
66
|
+
{ name: "research", role: "Find where the config is loaded", status: "running" },
|
|
67
|
+
{ name: "review", role: "Check the error messages", status: "completed" },
|
|
68
|
+
],
|
|
69
|
+
task_count: 1,
|
|
70
|
+
cycle_mode: "accept-edits",
|
|
71
|
+
conversation_title: "Fix the config loader",
|
|
65
72
|
plan_tier: "Pro",
|
|
66
73
|
terminal_width: process.stdout.columns || 120,
|
|
67
74
|
};
|
package/src/lib/paths.js
CHANGED
|
@@ -8,7 +8,8 @@ const AGY_SETTINGS = path.join(os.homedir(), ".gemini", "antigravity-cli", "sett
|
|
|
8
8
|
|
|
9
9
|
const INSTALL_META_NAME = "install.json";
|
|
10
10
|
|
|
11
|
-
// Library files
|
|
11
|
+
// Library files the runtime needs (paths relative to src/lib). init joins them
|
|
12
|
+
// with src/runtime/statusline.js into the one installed statusline.js.
|
|
12
13
|
const RUNTIME_FILES = [
|
|
13
14
|
"shared.js",
|
|
14
15
|
"colors.js",
|
|
@@ -35,13 +36,16 @@ const PRIVATE_FILES = [
|
|
|
35
36
|
"statusline.config.json",
|
|
36
37
|
".exec-cache.json",
|
|
37
38
|
"last-payload.json",
|
|
39
|
+
// SYNC_CACHE_NAME in git.js: commit IDs of the repos you work in.
|
|
40
|
+
".git-sync-cache.json",
|
|
38
41
|
// LOG_NAME in log.js, spelled out because log.js requires this file.
|
|
39
42
|
"agy-powerline.log",
|
|
40
43
|
];
|
|
41
44
|
const PRIVATE_FILE_MODE = 0o600;
|
|
42
45
|
|
|
43
46
|
// Everything init or the runtime may create in the install dir. uninstall
|
|
44
|
-
// --delete removes only these, never the whole directory tree.
|
|
47
|
+
// --delete removes only these, never the whole directory tree. RUNTIME_FILES
|
|
48
|
+
// are listed for installs from 1.1.0 and earlier, which copied them separately.
|
|
45
49
|
const INSTALLED_FILES = [
|
|
46
50
|
"statusline.js",
|
|
47
51
|
"statusline.cmd",
|
package/src/lib/payload-dump.js
CHANGED
|
@@ -17,6 +17,7 @@ const TEXT = "text";
|
|
|
17
17
|
const TIME = "time";
|
|
18
18
|
const PATH = "path";
|
|
19
19
|
const BRANCH = "branch";
|
|
20
|
+
const TITLE = "title";
|
|
20
21
|
|
|
21
22
|
const QUOTA = {
|
|
22
23
|
remaining_fraction: NUMBER,
|
|
@@ -52,6 +53,11 @@ const SCHEMA = {
|
|
|
52
53
|
vcs: VCS,
|
|
53
54
|
sandbox: { enabled: BOOL, allow_network: BOOL },
|
|
54
55
|
artifact_count: NUMBER,
|
|
56
|
+
subagents: "subagents",
|
|
57
|
+
task_count: NUMBER,
|
|
58
|
+
cycle_mode: TEXT,
|
|
59
|
+
vim: { mode: TEXT },
|
|
60
|
+
conversation_title: TITLE,
|
|
55
61
|
terminal_width: NUMBER,
|
|
56
62
|
};
|
|
57
63
|
|
|
@@ -102,6 +108,18 @@ function pick(value, rule, seen) {
|
|
|
102
108
|
if (rule === BRANCH) {
|
|
103
109
|
return typeof value === "string" && value ? branchPlaceholder(value) : undefined;
|
|
104
110
|
}
|
|
111
|
+
if (rule === TITLE) {
|
|
112
|
+
// A title can name the user's work; the placeholder keeps its length.
|
|
113
|
+
return typeof value === "string" && value
|
|
114
|
+
? "title" + "x".repeat(Math.max(0, Math.min(200, value.length) - 5))
|
|
115
|
+
: undefined;
|
|
116
|
+
}
|
|
117
|
+
if (rule === "subagents") {
|
|
118
|
+
// Only the status: a subagent's name and role describe the user's work.
|
|
119
|
+
return Array.isArray(value)
|
|
120
|
+
? value.slice(0, 50).map((agent) => pick(agent, { status: TEXT }, seen) || {})
|
|
121
|
+
: undefined;
|
|
122
|
+
}
|
|
105
123
|
if (rule === "quota") {
|
|
106
124
|
if (typeof value !== "object") {
|
|
107
125
|
return undefined;
|
package/src/lib/session.js
CHANGED
|
@@ -175,6 +175,16 @@ function extractSession(data, config) {
|
|
|
175
175
|
plan: firstText(data.plan_tier),
|
|
176
176
|
agentState: firstText(data.agent_state),
|
|
177
177
|
artifacts: Math.floor(count(data.artifact_count)),
|
|
178
|
+
// Finished subagents stay in the list Antigravity sends; only running ones count.
|
|
179
|
+
subagents: Array.isArray(data.subagents)
|
|
180
|
+
? data.subagents.filter((agent) => agent?.status === "running").length
|
|
181
|
+
: 0,
|
|
182
|
+
tasks: Math.floor(count(data.task_count)),
|
|
183
|
+
// Left out in the default mode, and with Vim editing off.
|
|
184
|
+
approvalMode: firstText(data.cycle_mode),
|
|
185
|
+
vimMode: firstText(data.vim?.mode),
|
|
186
|
+
// Left out until the conversation has a title; kept to one line.
|
|
187
|
+
title: firstText(data.conversation_title).replace(/\s+/g, " ").trim(),
|
|
178
188
|
version: firstText(data.version),
|
|
179
189
|
sandbox: data.sandbox?.enabled === true,
|
|
180
190
|
sandboxNet: data.sandbox?.allow_network === true,
|
package/src/lib/widget-style.js
CHANGED
|
@@ -102,6 +102,9 @@ const WIDGET_TAGS = {
|
|
|
102
102
|
plan: { icon: "⭐", name: "Plan" },
|
|
103
103
|
agentState: { icon: "🟣", name: "Agent" },
|
|
104
104
|
artifacts: { icon: "📎", name: "Artifacts" },
|
|
105
|
+
tasks: { icon: "👥", name: "" },
|
|
106
|
+
mode: { icon: "🧭", name: "" },
|
|
107
|
+
title: { icon: "💬", name: "" },
|
|
105
108
|
ram: { icon: "💻", name: "RAM" },
|
|
106
109
|
version: { icon: "🔖", name: "" },
|
|
107
110
|
sandbox: { icon: "🔒", name: "" },
|
|
@@ -127,6 +130,9 @@ function widgetTag(id, { emojis = true, raw = false, icon } = {}) {
|
|
|
127
130
|
return `${icon ?? tag.icon} ${tag.iconOnly ? "" : named}`;
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
// Longest conversation title shown, in terminal columns.
|
|
134
|
+
const TITLE_MAX = 30;
|
|
135
|
+
|
|
130
136
|
// RAM use (%) that turns the ram widget to the caution, then danger color.
|
|
131
137
|
const RAM_CAUTION = 80;
|
|
132
138
|
const RAM_DANGER = 90;
|
|
@@ -154,4 +160,5 @@ module.exports = {
|
|
|
154
160
|
hasNoData,
|
|
155
161
|
RAM_CAUTION,
|
|
156
162
|
RAM_DANGER,
|
|
163
|
+
TITLE_MAX,
|
|
157
164
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const fs = require("fs");
|
|
3
2
|
const path = require("path");
|
|
4
3
|
const { readConfigFile, renderStatusline } = require("./shared");
|
|
5
4
|
const { isTrustedInstallDir } = require("./paths");
|
|
@@ -17,6 +16,12 @@ const {
|
|
|
17
16
|
// A refresh slower than this is worth a line in the problem log.
|
|
18
17
|
const SLOW_RENDER_MS = 750;
|
|
19
18
|
|
|
19
|
+
// Antigravity can start the command and keep stdin open without writing to it
|
|
20
|
+
// (google-antigravity/antigravity-cli#698). A blocking read would wait until
|
|
21
|
+
// Antigravity kills the script, and a few kills in a row turn the custom
|
|
22
|
+
// statusline off, so the read gives up after this long.
|
|
23
|
+
const STDIN_TIMEOUT_MS = 1000;
|
|
24
|
+
|
|
20
25
|
// The config decides whether problems are logged, so it is read first. A
|
|
21
26
|
// broken config can't say, so logging stays on (AGY_POWERLINE_LOG=0 still
|
|
22
27
|
// turns it off).
|
|
@@ -32,58 +37,87 @@ process.on("uncaughtException", (err) => {
|
|
|
32
37
|
process.exit(0);
|
|
33
38
|
});
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Resolves with what stdin sent and whether it closed before the deadline.
|
|
41
|
+
function readStdin(timeoutMs) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
let done = false;
|
|
45
|
+
const finish = (complete) => {
|
|
46
|
+
if (done) return;
|
|
47
|
+
done = true;
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
// An open stdin would keep the process alive after the bar is printed.
|
|
50
|
+
process.stdin.destroy();
|
|
51
|
+
resolve({ text: Buffer.concat(chunks).toString("utf8"), complete });
|
|
52
|
+
};
|
|
53
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
54
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
55
|
+
process.stdin.on("end", () => finish(true));
|
|
56
|
+
process.stdin.on("error", () => finish(true));
|
|
57
|
+
});
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
(process.env.AGY_POWERLINE_DEBUG ? path.join(__dirname, "last-payload.json") : null);
|
|
48
|
-
if (dumpPath) {
|
|
49
|
-
writePayloadDump(dumpPath, input);
|
|
50
|
-
}
|
|
60
|
+
readStdin(STDIN_TIMEOUT_MS).then(({ text, complete }) => {
|
|
61
|
+
render(text.replace(/^\uFEFF/, "").trim(), complete);
|
|
62
|
+
});
|
|
51
63
|
|
|
52
|
-
|
|
64
|
+
function render(input, complete) {
|
|
65
|
+
if (!input) {
|
|
66
|
+
if (!complete) {
|
|
67
|
+
logWarning("STDIN_TIMEOUT", { ms: STDIN_TIMEOUT_MS });
|
|
68
|
+
}
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
53
71
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
process.exit(0);
|
|
62
|
-
}
|
|
72
|
+
// Debug dumps keep only the fields the bar renders, never personal ones.
|
|
73
|
+
const dumpPath =
|
|
74
|
+
dumpTarget(process.env.AGY_POWERLINE_DUMP_PAYLOAD, __dirname) ||
|
|
75
|
+
(process.env.AGY_POWERLINE_DEBUG ? path.join(__dirname, "last-payload.json") : null);
|
|
76
|
+
if (dumpPath) {
|
|
77
|
+
writePayloadDump(dumpPath, input);
|
|
78
|
+
}
|
|
63
79
|
|
|
64
|
-
|
|
65
|
-
if (typeof data?.version === "string" && /^\d[\w.+-]{0,30}$/.test(data.version)) {
|
|
66
|
-
setLogContext({ antigravity: data.version });
|
|
67
|
-
}
|
|
80
|
+
let data;
|
|
68
81
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
try {
|
|
83
|
+
data = JSON.parse(input);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// Stdin was cut off by the deadline: nothing worth showing.
|
|
86
|
+
if (!complete) {
|
|
87
|
+
logWarning("STDIN_TIMEOUT", { ms: STDIN_TIMEOUT_MS });
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
logWarning("PAYLOAD_INVALID_JSON", { message: jsonErrorMessage(err.message) });
|
|
91
|
+
// Node's message quotes the bad input; only say where it broke.
|
|
92
|
+
process.stderr.write(`[agy-powerline] ${jsonErrorMessage(err.message)} on stdin\n`);
|
|
93
|
+
process.stdout.write("\x1b[31m[agy-powerline: invalid JSON]\x1b[0m\n");
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
process.stderr.write(`[agy-powerline] invalid statusline.config.json: ${configError}\n`);
|
|
79
|
-
}
|
|
97
|
+
// The one payload value the log keeps: Antigravity's version number.
|
|
98
|
+
if (typeof data?.version === "string" && /^\d[\w.+-]{0,30}$/.test(data.version)) {
|
|
99
|
+
setLogContext({ antigravity: data.version });
|
|
100
|
+
}
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
// The config lives next to this script. A copy anywhere other than the
|
|
103
|
+
// global install or AGY_POWERLINE_HOME (e.g. committed inside a repo) may be
|
|
104
|
+
// edited by someone else, so its exec widgets stay off.
|
|
105
|
+
const exec = isTrustedInstallDir(__dirname) ? "run" : "blocked";
|
|
84
106
|
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
if (configError) {
|
|
108
|
+
logWarning("CONFIG_INVALID", {
|
|
109
|
+
message: /JSON/.test(configError) ? jsonErrorMessage(configError) : redact(configError),
|
|
110
|
+
});
|
|
111
|
+
process.stderr.write(`[agy-powerline] invalid statusline.config.json: ${configError}\n`);
|
|
112
|
+
}
|
|
88
113
|
|
|
89
|
-
|
|
114
|
+
const started = Date.now();
|
|
115
|
+
const output = renderStatusline(data, config, { exec, configError });
|
|
116
|
+
const elapsed = Date.now() - started;
|
|
117
|
+
|
|
118
|
+
if (elapsed > SLOW_RENDER_MS) {
|
|
119
|
+
logWarning("SLOW_RENDER", { ms: elapsed, widgets: config.widgets.map((widget) => widget.id) });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
process.stdout.write(output);
|
|
123
|
+
}
|