@llblab/pi-telegram 0.43.2 → 0.45.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/AGENTS.md +14 -9
- package/BACKLOG.md +20 -4
- package/CHANGELOG.md +22 -5
- package/README.md +13 -9
- package/docs/README.md +1 -0
- package/docs/architecture.md +220 -18
- package/docs/generative-apps.md +1 -1
- package/docs/multi-instance-bus.md +70 -19
- package/docs/outbound.md +13 -7
- package/docs/public-api.md +13 -5
- package/docs/ui-style.md +3 -1
- package/index.ts +4 -1415
- package/lib/activity.ts +19 -5
- package/lib/agent-messages.ts +6 -3
- package/lib/bindings.ts +37 -2
- package/lib/bus-follower.ts +600 -135
- package/lib/bus-leader.ts +962 -55
- package/lib/bus.ts +350 -26
- package/lib/channel-posts.ts +544 -0
- package/lib/commands.ts +234 -11
- package/lib/config.ts +178 -25
- package/lib/delivery.ts +18 -18
- package/lib/extension.ts +1792 -0
- package/lib/generative-apps.ts +20 -2
- package/lib/journal.ts +2184 -126
- package/lib/lifecycle.ts +7 -1
- package/lib/locks.ts +38 -1
- package/lib/menu-settings.ts +154 -15
- package/lib/outbound-attachments.ts +74 -40
- package/lib/outbound-voice.ts +28 -42
- package/lib/outbound.ts +18 -14
- package/lib/paths.ts +29 -0
- package/lib/polling.ts +85 -17
- package/lib/preview.ts +115 -70
- package/lib/prompts.ts +5 -2
- package/lib/queue.ts +66 -22
- package/lib/replies.ts +47 -39
- package/lib/routing.ts +305 -112
- package/lib/status.ts +10 -0
- package/lib/sync.ts +308 -39
- package/lib/telegram-api.ts +315 -7
- package/lib/thread-cleanup-manager.ts +664 -0
- package/lib/thread-display.ts +226 -0
- package/lib/thread-naming.ts +118 -0
- package/lib/threads.ts +1686 -129
- package/lib/updates.ts +1319 -97
- package/lib/workspace-admission.ts +1643 -0
- package/lib/workspace-retirement.ts +968 -0
- package/lib/workspace-slots.ts +84 -0
- package/package.json +1 -1
- package/screenshot.png +0 -0
- package/scripts/measure-bus.mjs +83 -0
- package/scripts/measure-workspace.mjs +101 -0
- package/skills/show-me/SKILL.md +166 -0
- package/skills/show-me/references/telegram-surfaces.md +43 -0
- package/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace slot allocation policy
|
|
3
|
+
* Zones: telegram, workspace identity
|
|
4
|
+
* Owns bounded profile-wide letter selection and inactivity ordering.
|
|
5
|
+
* Excludes liveness discovery, persistence, routing, and Telegram deletion;
|
|
6
|
+
* a selection is a proposal, never authority to retire a binding.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const TELEGRAM_WORKSPACE_SLOTS = "abcdefghijklmnopqrstuvwxyz";
|
|
10
|
+
|
|
11
|
+
export interface TelegramWorkspaceSlotOccupancy {
|
|
12
|
+
bindingKey: string;
|
|
13
|
+
slot: string;
|
|
14
|
+
inactiveSinceMs?: number;
|
|
15
|
+
protection: "eligible" | "protected" | "unknown";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type TelegramWorkspaceSlotAllocation =
|
|
19
|
+
| { kind: "free"; slot: string }
|
|
20
|
+
| { kind: "reclaim"; candidate: TelegramWorkspaceSlotOccupancy }
|
|
21
|
+
| { kind: "blocked"; reason: "invalid-state" | "protected-capacity" };
|
|
22
|
+
|
|
23
|
+
function isSlot(slot: string): boolean {
|
|
24
|
+
return /^[a-z]$/u.test(slot);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isValidSnapshot(
|
|
28
|
+
bindings: readonly TelegramWorkspaceSlotOccupancy[],
|
|
29
|
+
reservedSlots: readonly string[],
|
|
30
|
+
nowMs: number,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (!Number.isFinite(nowMs) || nowMs < 0) return false;
|
|
33
|
+
const slots = new Set<string>();
|
|
34
|
+
const keys = new Set<string>();
|
|
35
|
+
for (const binding of bindings) {
|
|
36
|
+
if (!isSlot(binding.slot) || !binding.bindingKey ||
|
|
37
|
+
slots.has(binding.slot) || keys.has(binding.bindingKey)) return false;
|
|
38
|
+
slots.add(binding.slot);
|
|
39
|
+
keys.add(binding.bindingKey);
|
|
40
|
+
}
|
|
41
|
+
return reservedSlots.every(isSlot);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function eligibleByInactivity(
|
|
45
|
+
bindings: readonly TelegramWorkspaceSlotOccupancy[],
|
|
46
|
+
reservedSlots: readonly string[],
|
|
47
|
+
nowMs: number,
|
|
48
|
+
): TelegramWorkspaceSlotOccupancy[] {
|
|
49
|
+
const reserved = new Set(reservedSlots);
|
|
50
|
+
return bindings.filter((binding) =>
|
|
51
|
+
binding.protection === "eligible" &&
|
|
52
|
+
!reserved.has(binding.slot) &&
|
|
53
|
+
typeof binding.inactiveSinceMs === "number" &&
|
|
54
|
+
Number.isFinite(binding.inactiveSinceMs) &&
|
|
55
|
+
binding.inactiveSinceMs >= 0 &&
|
|
56
|
+
binding.inactiveSinceMs <= nowMs,
|
|
57
|
+
).sort((left, right) =>
|
|
58
|
+
left.inactiveSinceMs! - right.inactiveSinceMs! ||
|
|
59
|
+
left.slot.charCodeAt(0) - right.slot.charCodeAt(0),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Caller must recheck exact ownership and protected work before retirement. */
|
|
64
|
+
export function planTelegramWorkspaceSlotAllocation(input: {
|
|
65
|
+
bindings: readonly TelegramWorkspaceSlotOccupancy[];
|
|
66
|
+
reservedSlots: readonly string[];
|
|
67
|
+
nowMs: number;
|
|
68
|
+
}): TelegramWorkspaceSlotAllocation {
|
|
69
|
+
const { bindings, reservedSlots, nowMs } = input;
|
|
70
|
+
if (!isValidSnapshot(bindings, reservedSlots, nowMs)) {
|
|
71
|
+
return { kind: "blocked", reason: "invalid-state" };
|
|
72
|
+
}
|
|
73
|
+
const occupied = new Set([
|
|
74
|
+
...bindings.map((binding) => binding.slot),
|
|
75
|
+
...reservedSlots,
|
|
76
|
+
]);
|
|
77
|
+
for (const slot of TELEGRAM_WORKSPACE_SLOTS) {
|
|
78
|
+
if (!occupied.has(slot)) return { kind: "free", slot };
|
|
79
|
+
}
|
|
80
|
+
const candidate = eligibleByInactivity(bindings, reservedSlots, nowMs)[0];
|
|
81
|
+
return candidate
|
|
82
|
+
? { kind: "reclaim", candidate: { ...candidate } }
|
|
83
|
+
: { kind: "blocked", reason: "protected-capacity" };
|
|
84
|
+
}
|
package/package.json
CHANGED
package/screenshot.png
CHANGED
|
Binary file
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Counts isolated registry work without sockets, configured profiles, or Telegram calls.
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createTelegramBusFollowerRegistry, createTelegramBusProtocolIdentity } from "../lib/bus.ts";
|
|
4
|
+
|
|
5
|
+
const repetitions = 10;
|
|
6
|
+
const methods = ["get", "set", "delete", "entries", "values"];
|
|
7
|
+
const originals = Object.fromEntries(methods.map((name) => [name, Map.prototype[name]]));
|
|
8
|
+
const rows = [];
|
|
9
|
+
let counts;
|
|
10
|
+
|
|
11
|
+
function measure(size, operation, execute) {
|
|
12
|
+
const sample = { gets: 0, sets: 0, deletes: 0, entryVisits: 0, valueVisits: 0 };
|
|
13
|
+
let result;
|
|
14
|
+
counts = sample;
|
|
15
|
+
try {
|
|
16
|
+
for (let index = 0; index < repetitions; index++) result = execute(index);
|
|
17
|
+
} finally {
|
|
18
|
+
counts = undefined;
|
|
19
|
+
}
|
|
20
|
+
rows.push({ size, operation, repetitions, ...sample });
|
|
21
|
+
return { result, counts: sample };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
for (const [method, counter] of [["get", "gets"], ["set", "sets"], ["delete", "deletes"]]) {
|
|
26
|
+
Map.prototype[method] = function (...args) {
|
|
27
|
+
if (counts) counts[counter]++;
|
|
28
|
+
return originals[method].apply(this, args);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
for (const [method, counter] of [["entries", "entryVisits"], ["values", "valueVisits"]]) {
|
|
32
|
+
Map.prototype[method] = function* (...args) {
|
|
33
|
+
for (const entry of originals[method].apply(this, args)) {
|
|
34
|
+
if (counts) counts[counter]++;
|
|
35
|
+
yield entry;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
for (const size of [1, 13, 26]) {
|
|
40
|
+
const registry = createTelegramBusFollowerRegistry();
|
|
41
|
+
const protocol = createTelegramBusProtocolIdentity({ runtimeBuild: "measurement" });
|
|
42
|
+
const registrations = Array.from({ length: size }, (_, index) => ({
|
|
43
|
+
instanceId: `fixture-${index}`, profileKey: `manual:fixture-${index}`,
|
|
44
|
+
registrationGeneration: `generation-${index}`, connectedAtMs: 1000,
|
|
45
|
+
target: { chatId: 7, threadId: index + 1 }, protocol,
|
|
46
|
+
}));
|
|
47
|
+
for (const registration of registrations) registry.register(registration);
|
|
48
|
+
const last = registrations.at(-1);
|
|
49
|
+
const registered = measure(size, "reregister-last", () => registry.register(last));
|
|
50
|
+
assert.equal(registered.counts.entryVisits, size * repetitions);
|
|
51
|
+
const heartbeat = measure(size, "heartbeat-last", (index) => registry.heartbeat(last.instanceId, 2000 + index));
|
|
52
|
+
assert.equal(heartbeat.result.lastHeartbeatMs, 2000 + repetitions - 1);
|
|
53
|
+
assert.equal(heartbeat.counts.entryVisits + heartbeat.counts.valueVisits, 0);
|
|
54
|
+
assert.equal(heartbeat.counts.gets, repetitions);
|
|
55
|
+
assert.equal(heartbeat.counts.sets, repetitions);
|
|
56
|
+
const first = measure(size, "target-first", () => registry.getByTarget(registrations[0].target));
|
|
57
|
+
assert.equal(first.result.instanceId, registrations[0].instanceId);
|
|
58
|
+
assert.equal(first.counts.valueVisits, repetitions);
|
|
59
|
+
const tail = measure(size, "target-last", () => registry.getByTarget(last.target));
|
|
60
|
+
assert.equal(tail.result.instanceId, last.instanceId);
|
|
61
|
+
assert.equal(tail.counts.valueVisits, size * repetitions);
|
|
62
|
+
const missing = measure(size, "target-missing-chat", () => registry.getByTarget({ chatId: 8, threadId: size }));
|
|
63
|
+
assert.equal(missing.result, undefined);
|
|
64
|
+
assert.equal(missing.counts.valueVisits, size * repetitions);
|
|
65
|
+
const roster = measure(size, "list", () => registry.list());
|
|
66
|
+
assert.equal(roster.result.length, size);
|
|
67
|
+
assert.equal(roster.counts.valueVisits, size * repetitions);
|
|
68
|
+
for (const view of [registered.result, heartbeat.result, first.result, tail.result, ...roster.result]) {
|
|
69
|
+
const expected = registry.get(view.instanceId);
|
|
70
|
+
view.target.chatId = 99;
|
|
71
|
+
view.protocol.capabilities.push("fixture-only");
|
|
72
|
+
assert.deepEqual(registry.get(view.instanceId), expected, "Returned views must not mutate registry authority");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
for (const method of methods) Map.prototype[method] = originals[method];
|
|
77
|
+
}
|
|
78
|
+
console.log(JSON.stringify({
|
|
79
|
+
scope: "Synchronous isolated follower registry, not IPC, authentication, provisioning, or throughput",
|
|
80
|
+
counters: "Aggregate Map operations and visited entries for each row's repetitions; no allocation or timing claims",
|
|
81
|
+
copyEvidence: "Mutating returned target/protocol capability views leaves registry authority unchanged",
|
|
82
|
+
rows,
|
|
83
|
+
}, null, 2));
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Measures isolated Thread-store work; never reads configured profiles or calls Telegram.
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { createTelegramTopicTargetStore } from "../lib/threads.ts";
|
|
8
|
+
|
|
9
|
+
const repetitions = 10;
|
|
10
|
+
const original = { readFile: fs.readFile, writeFile: fs.writeFile, mkdir: fs.mkdir, rename: fs.rename,
|
|
11
|
+
parse: JSON.parse, stringify: JSON.stringify };
|
|
12
|
+
let counts;
|
|
13
|
+
const rows = [];
|
|
14
|
+
const root = await fs.mkdtemp(join(tmpdir(), "pi-telegram-measure-workspace-"));
|
|
15
|
+
|
|
16
|
+
async function measure(size, operation, execute, samples = repetitions) {
|
|
17
|
+
counts = { reads: 0, readBytes: 0, writes: 0, writeBytes: 0, mkdirs: 0, renames: 0,
|
|
18
|
+
parses: 0, stringifies: 0 };
|
|
19
|
+
try {
|
|
20
|
+
for (let index = 0; index < samples; index++) await execute(index);
|
|
21
|
+
const result = { size, operation, repetitions: samples, ...counts };
|
|
22
|
+
rows.push(result);
|
|
23
|
+
return result;
|
|
24
|
+
} finally {
|
|
25
|
+
counts = undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
fs.readFile = async (...args) => {
|
|
31
|
+
if (counts) counts.reads++;
|
|
32
|
+
const result = await original.readFile(...args);
|
|
33
|
+
if (counts) counts.readBytes += Buffer.byteLength(result);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
fs.writeFile = async (...args) => {
|
|
37
|
+
if (counts) { counts.writes++; counts.writeBytes += Buffer.byteLength(args[1]); }
|
|
38
|
+
return original.writeFile(...args);
|
|
39
|
+
};
|
|
40
|
+
fs.mkdir = async (...args) => {
|
|
41
|
+
if (counts) counts.mkdirs++;
|
|
42
|
+
return original.mkdir(...args);
|
|
43
|
+
};
|
|
44
|
+
fs.rename = async (...args) => {
|
|
45
|
+
if (counts) counts.renames++;
|
|
46
|
+
return original.rename(...args);
|
|
47
|
+
};
|
|
48
|
+
JSON.parse = (...args) => { if (counts) counts.parses++; return original.parse(...args); };
|
|
49
|
+
JSON.stringify = (...args) => { if (counts) counts.stringifies++; return original.stringify(...args); };
|
|
50
|
+
syncBuiltinESMExports();
|
|
51
|
+
|
|
52
|
+
for (const size of [1, 13, 26]) {
|
|
53
|
+
const path = join(root, `${size}.json`);
|
|
54
|
+
const store = createTelegramTopicTargetStore({ path, getNowMs: () => 1000 });
|
|
55
|
+
for (let index = 0; index < size; index++) {
|
|
56
|
+
const instanceId = `fixture-${index}`;
|
|
57
|
+
const identity = store.claimWorkspaceIdentity(`/fixture/${index}`, instanceId);
|
|
58
|
+
assert.ok(identity);
|
|
59
|
+
const target = { chatId: 7, threadId: index + 1 };
|
|
60
|
+
assert.ok(store.upsertWorkspaceBinding({ ...identity, target, updatedAtMs: 1000 }, instanceId));
|
|
61
|
+
store.upsert({ profileKey: `manual:${instanceId}`, instanceId, slot: identity.slot,
|
|
62
|
+
target, status: "active", createdAtMs: 1000, updatedAtMs: 1000 });
|
|
63
|
+
}
|
|
64
|
+
await store.persist();
|
|
65
|
+
assert.equal(new Set(store.listWorkspaceBindings().map((entry) => entry.slot)).size, size);
|
|
66
|
+
await measure(size, "cold-load", async () => {
|
|
67
|
+
const reader = createTelegramTopicTargetStore({ path });
|
|
68
|
+
await reader.load();
|
|
69
|
+
assert.equal(reader.list().length, size);
|
|
70
|
+
assert.equal(reader.listWorkspaceBindings().length, size);
|
|
71
|
+
});
|
|
72
|
+
const lookup = await measure(size, "lookup-last", () => {
|
|
73
|
+
assert.equal(store.getByProfileKey(`manual:fixture-${size - 1}`)?.target.threadId, size);
|
|
74
|
+
});
|
|
75
|
+
assert.equal(lookup.reads + lookup.writes, 0);
|
|
76
|
+
const beforeReloadSave = JSON.parse(await fs.readFile(path, "utf8"));
|
|
77
|
+
const reloaded = await measure(size, "first-persist-after-reload", () => store.persist(), 1);
|
|
78
|
+
assert.equal(reloaded.writes + reloaded.renames + reloaded.mkdirs, 0);
|
|
79
|
+
assert.deepEqual(JSON.parse(await fs.readFile(path, "utf8")), beforeReloadSave,
|
|
80
|
+
"The first reloaded save must preserve the seeded semantic state");
|
|
81
|
+
const unchanged = await measure(size, "unchanged-persist", () => store.persist());
|
|
82
|
+
assert.equal(unchanged.writes + unchanged.renames + unchanged.mkdirs, 0);
|
|
83
|
+
assert.ok(unchanged.reads >= repetitions, "No-op saves still consult disk authority");
|
|
84
|
+
const changed = await measure(size, "diagnostic-persist", async (index) => {
|
|
85
|
+
store.setStatusSnapshot({ diagnostics: { measurementStep: index } });
|
|
86
|
+
await store.persist();
|
|
87
|
+
});
|
|
88
|
+
assert.equal(changed.writes, repetitions);
|
|
89
|
+
assert.equal(changed.renames, repetitions);
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
Object.assign(fs, { readFile: original.readFile, writeFile: original.writeFile,
|
|
93
|
+
mkdir: original.mkdir, rename: original.rename });
|
|
94
|
+
JSON.parse = original.parse;
|
|
95
|
+
JSON.stringify = original.stringify;
|
|
96
|
+
syncBuiltinESMExports();
|
|
97
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
console.log(JSON.stringify({ scope: "Isolated Thread store, not IPC/admission/Telegram or throughput",
|
|
100
|
+
counters: "Aggregate API calls and bytes for each row's repetitions; object-spread clones are not instrumented",
|
|
101
|
+
rows }, null, 2));
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: show-me
|
|
3
|
+
description: Explain completed work, proposed changes, system structure, or ideas through truthful contextual Markdown, concise diagrams, code-shape sketches, and focused HTML artifacts. Use when the user asks to show what happened, what changed, or how something works, especially on a phone-width Telegram surface.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Help the user understand the current topic. Skip the preamble and keep prose brief. Pick the smallest view that makes the key point clear without making the underlying claim less true.
|
|
7
|
+
|
|
8
|
+
### Output Selection
|
|
9
|
+
|
|
10
|
+
- Infer the subject from the conversation and honor an explicitly requested format. With plain `show me`, choose the form that best explains the subject.
|
|
11
|
+
- Markdown in the reply is a complete output format for both chat and terminal surfaces. Use headings, emphasis, lists, and focused code blocks to explain outcomes, comparisons, and reasoning; add a diagram when relationships need one.
|
|
12
|
+
- `Show me markdown` requests a rendered Markdown reply. Create a Markdown file only when the user asks for a saved document or file artifact.
|
|
13
|
+
- `Show me html` requests a focused HTML file. Save file artifacts in the project or filesystem as appropriate and deliver them through the active environment's file-delivery mechanism; open locally when that is the requested surface.
|
|
14
|
+
|
|
15
|
+
### Surface Routing
|
|
16
|
+
|
|
17
|
+
- Infer whether the active surface is Telegram, a terminal, or another client. Keep the explanation portable and use surface-specific delivery only when the context or user request authorizes it.
|
|
18
|
+
- For Telegram output, read [`references/telegram-surfaces.md`](./references/telegram-surfaces.md) before selecting Markdown or HTML. Keep the immediate reply useful even when an attached artifact is the deeper view.
|
|
19
|
+
- When asked what changed or happened in current work, inspect the retained diff, status, and validation evidence when available. Separate this task from pre-existing changes and distinguish repository state from released or live behavior.
|
|
20
|
+
|
|
21
|
+
### Truth And State
|
|
22
|
+
|
|
23
|
+
- Label material claims as `live`, `released`, `locally implemented`, `proposed`, or `unverified` when the distinction affects interpretation. Omit redundant labels when the state is already unambiguous.
|
|
24
|
+
- Preserve the exact mechanism, owner, and boundary when simplifying technical behavior. A friendly label may supplement the implementation term but must not replace it when that would merge distinct timers, states, stages, or authorities.
|
|
25
|
+
- Make a visual diff correspond to the actual changed symbol or contract. Show unchanged neighboring behavior when omission could imply that it changed too.
|
|
26
|
+
- Do not infer that an affordance is clickable, interactive, rendered, or client-visible from source or markup shape alone. Distinguish designed or expected behavior from behavior verified through the relevant renderer, transport, runtime, and client.
|
|
27
|
+
- Treat a mockup, diagram, or rendered artifact as explanatory evidence, not runtime proof. State the unverified boundary when the user could reasonably mistake one for the other.
|
|
28
|
+
|
|
29
|
+
A compact state line is enough when provenance matters:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
State: locally implemented · validated · not released · not live
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Visual Forms
|
|
36
|
+
|
|
37
|
+
- Show logic or an algorithm as pseudocode:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
on(save)
|
|
41
|
+
if content is unchanged
|
|
42
|
+
return cached result
|
|
43
|
+
write new content
|
|
44
|
+
return fresh result
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- Show runtime control flow as a call tree:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
submitForm
|
|
51
|
+
createSession
|
|
52
|
+
persistPrompt
|
|
53
|
+
launchAgent
|
|
54
|
+
navigateToSession
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- Show UI structure as a component tree, including state and module boundaries that matter:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
<SessionPage> (apps/example/src/routes/session.tsx)
|
|
61
|
+
useSessionEvents()
|
|
62
|
+
<SessionToolbar>
|
|
63
|
+
<RunSkillButton> (packages/ui)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- Show file responsibility or a broad refactor as a shallow file tree:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
src/
|
|
70
|
+
├─ commands/
|
|
71
|
+
│ └─ parses user actions
|
|
72
|
+
├─ sessions/
|
|
73
|
+
│ └─ owns session state
|
|
74
|
+
└─ transport/
|
|
75
|
+
└─ sends API requests
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- Show component interaction, control flow, or data flow with Mermaid:
|
|
79
|
+
|
|
80
|
+
```mermaid
|
|
81
|
+
sequenceDiagram
|
|
82
|
+
participant User
|
|
83
|
+
participant UI
|
|
84
|
+
participant Daemon
|
|
85
|
+
User->>UI: choose command
|
|
86
|
+
UI->>Daemon: send expanded prompt
|
|
87
|
+
Daemon-->>UI: stream result
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- Use `diff` when the point is what changes and the surrounding shape already exists. Match the diff shape to the topic.
|
|
91
|
+
|
|
92
|
+
For a component change:
|
|
93
|
+
|
|
94
|
+
```diff
|
|
95
|
+
<SessionPage>
|
|
96
|
+
useSessionEvents()
|
|
97
|
+
<SessionToolbar>
|
|
98
|
+
+ <RunSkillButton />
|
|
99
|
+
<SessionTimeline>
|
|
100
|
+
+ <SkillResultCard />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For a file-layout change:
|
|
104
|
+
|
|
105
|
+
```diff
|
|
106
|
+
src/
|
|
107
|
+
├─ commands/
|
|
108
|
+
+│ └─ show-me.ts
|
|
109
|
+
+│ └─ expands the slash command
|
|
110
|
+
├─ sessions/
|
|
111
|
+
-└─ transport.ts
|
|
112
|
+
+└─ transport/
|
|
113
|
+
+ ├─ client.ts
|
|
114
|
+
+ └─ stream.ts
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For a call-tree or call-stack change:
|
|
118
|
+
|
|
119
|
+
```diff
|
|
120
|
+
submitForm
|
|
121
|
+
createSession
|
|
122
|
+
persistPrompt
|
|
123
|
+
+ expandSkillMention
|
|
124
|
+
launchAgent
|
|
125
|
+
- navigateToSession
|
|
126
|
+
+ navigateToSession
|
|
127
|
+
+ subscribeToEvents
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
For a state or control-flow change:
|
|
131
|
+
|
|
132
|
+
```diff
|
|
133
|
+
on(save)
|
|
134
|
+
- write content
|
|
135
|
+
+ if content is unchanged
|
|
136
|
+
+ return cached result
|
|
137
|
+
+ write new content
|
|
138
|
+
+ invalidate cache
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- Show the whole block when most of it is new, when omitted context would hide ownership or order, or when the user needs a copyable target shape:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
function expandSkill(command: string): string {
|
|
145
|
+
const skillName = command.slice(1)
|
|
146
|
+
return `use the ${skillName} skill`
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- For a visual UI, layout, state comparison, or concept too dense for Mermaid, use a focused HTML artifact — a diagram, an infographic, or a short slide deck, whichever fits the point. Match the product's colors, type, spacing, and components; use real labels and data; support desktop and mobile. When practical, render representative narrow and wide viewports and report what was actually inspected.
|
|
151
|
+
|
|
152
|
+
### Text Rendering
|
|
153
|
+
|
|
154
|
+
- Adapt the view to the available width in both terminal and chat surfaces. Use shallow trees with `├─`, `└─`, and `│`, a space before labels, and a three-column nesting step for file hierarchy; use indentation for call trees and pseudocode.
|
|
155
|
+
- Keep labels concise and in the user's language. Express status with words; use text glyphs with predictable monospace width for aligned diagram structure.
|
|
156
|
+
- For changes, use a compact fenced `diff` block in Telegram or the terminal. Show the changed lines and only the surrounding context needed to understand them.
|
|
157
|
+
- In trees, place comments and explanations as child nodes one level below the item they describe. Keep the item's own line for its label.
|
|
158
|
+
- Use prose lists for independent statuses and split larger views into meaningful sections.
|
|
159
|
+
|
|
160
|
+
### Guidance
|
|
161
|
+
|
|
162
|
+
Place each visual next to the short text it supports. Keep only the calls, files, props, states, and boundaries needed to answer the user's current question or the options to resolve the current discussion point. Prefer one primary visual and a few decision-relevant takeaways; add another form only when it explains a different necessary relationship.
|
|
163
|
+
|
|
164
|
+
Use source paths, symbol names, or validation evidence when they materially anchor a claim, not as decoration. Report the strongest state the evidence supports and no stronger.
|
|
165
|
+
|
|
166
|
+
You may use one of these, you may use several, it is unlikely you will use all of them. Use your judgement and don't overwhelm the user.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Telegram Explanation Surfaces
|
|
2
|
+
|
|
3
|
+
Use this reference only when Show Me is responding through Telegram or preparing an artifact for Telegram delivery.
|
|
4
|
+
|
|
5
|
+
## Selection
|
|
6
|
+
|
|
7
|
+
- With plain `show me`, prefer a native Markdown reply when one phone-width view can explain the point.
|
|
8
|
+
- Use HTML when spatial comparison, dense state, a timeline, or a visual hierarchy would become harder to understand in narrow Markdown.
|
|
9
|
+
- `Show me markdown` means rendered Markdown in the current reply, not a `.md` attachment, unless the user explicitly asks for a file.
|
|
10
|
+
- `Show me html` means one focused, self-contained `.html` artifact delivered through the active Telegram file path. The surrounding reply should say what the artifact explains and disclose its evidence state.
|
|
11
|
+
- Do not create both formats by habit. The second format must answer a need the first cannot.
|
|
12
|
+
|
|
13
|
+
## Telegram Markdown
|
|
14
|
+
|
|
15
|
+
- Design for a phone before a desktop: one governing question, one primary visual, short labels, shallow nesting, and prose that wraps naturally.
|
|
16
|
+
- Prefer compact semantic diffs, call trees, timelines, or state transitions over raw repository diffs. A remote user needs to understand impact before file-level detail.
|
|
17
|
+
- Avoid wide tables, deep trees, side-by-side layouts, and Mermaid when the current Telegram renderer would expose only source text. Move genuinely spatial material to HTML.
|
|
18
|
+
- Put material state near the top: what changed, whether it is local or live, what was validated, and what remains unresolved.
|
|
19
|
+
- Keep source paths and symbol names below the explanation unless they are the explanation.
|
|
20
|
+
- Keep intended Telegram bot-command tokens as plain text rather than inline code. Format Pi/TUI or shell commands as code according to the host contract. Plain source shape does not prove native clickability; claim it only after the active entity-detection and client path is verified.
|
|
21
|
+
- Use explicit Markdown links when a destination matters rather than assuming plain URL auto-detection.
|
|
22
|
+
|
|
23
|
+
## Telegram HTML
|
|
24
|
+
|
|
25
|
+
- Produce a single self-contained file with UTF-8 metadata and a viewport declaration. Avoid external assets, scripts, fonts, trackers, or network requirements unless the user explicitly requested them.
|
|
26
|
+
- Build mobile-first for roughly phone-width reading, then let the same document expand cleanly in a system or desktop browser. Text must wrap; diagrams must scroll or reflow without clipping.
|
|
27
|
+
- Use semantic headings, sufficient contrast, non-color status meaning, comfortable touch targets, and no hover-only information.
|
|
28
|
+
- Preserve real labels, values, ordering, and uncertainty. An attractive reconstruction must not invent runtime state or imply that a proposed interaction exists.
|
|
29
|
+
- Include a compact provenance line when state matters, such as `Local patch · validated · not released · not live`.
|
|
30
|
+
- When rendering tools are available, inspect at least one narrow viewport and one wider viewport. Report what was inspected; static source review is not visual proof.
|
|
31
|
+
- Deliver the file through the active Telegram attachment mechanism. Do not expose local paths as if the user could open them remotely.
|
|
32
|
+
|
|
33
|
+
## Current-Work Evidence
|
|
34
|
+
|
|
35
|
+
Before explaining “what we did” or “what happened,” use the narrowest available evidence that can support the answer:
|
|
36
|
+
|
|
37
|
+
1. Inspect retained repository status and diff for the relevant task.
|
|
38
|
+
2. Identify pre-existing or unrelated changes and exclude them from the claimed task result.
|
|
39
|
+
3. Name the actual changed mechanism or contract, not a friendlier neighboring concept.
|
|
40
|
+
4. Separate implementation evidence from validation, release, deployment, runtime, transport, and client evidence.
|
|
41
|
+
5. State unresolved causes or missing live checks instead of filling them with a cleaner story.
|
|
42
|
+
|
|
43
|
+
A visual is successful when the user can understand the outcome away from a computer without being given a stronger claim than the evidence supports.
|
|
@@ -7,7 +7,7 @@ Read this reference only for explicit local/TUI Telegram delivery, cross-target
|
|
|
7
7
|
Use `telegram_message` only when the user explicitly requests Telegram delivery from local/TUI or names a concrete different Telegram target.
|
|
8
8
|
|
|
9
9
|
- Omitted target selects the paired/default target only outside an active Telegram turn.
|
|
10
|
-
- `chat_id` plus optional `thread_id` selects an explicit Bot API target.
|
|
10
|
+
- `chat_id` plus optional `thread_id` selects an explicit Bot API target. A public `@username`, or an exact negative numeric channel ID with `channel: true`, uses `chat_id` without `thread_id`; channel delivery requires the direct leader, and Telegram enforces the bot's posting permission.
|
|
11
11
|
- `thread` selects another live Pi Thread by case-insensitive name or numeric id and admits one attributed turn there.
|
|
12
12
|
- During an active Telegram turn, answer the current target normally; direct delivery to that same target is rejected.
|
|
13
13
|
- Direct delivery requires this Pi instance to own transport or hold a live Threaded Mode registration.
|