Compare commits

...

31 Commits

Author SHA1 Message Date
Jeff Carr 6c58ab5e15 rm junk 2025-09-04 00:13:24 -05:00
Jeff Carr f5b513fa05 buttons for different stuff 2025-09-03 01:19:37 -05:00
Jeff Carr 785591ab4c dumps out the gemini struct enough for now 2025-09-02 18:36:59 -05:00
Jeff Carr 61179d6aae make a "books" window 2025-09-02 17:47:30 -05:00
Jeff Carr 589d50d950 convert is working okay 2025-09-02 15:18:27 -05:00
Jeff Carr 81343b3b66 remove files if it seems close enough 2025-09-02 01:21:24 -05:00
Jeff Carr db079664c9 preliminary import working 2025-09-01 22:19:33 -05:00
Jeff Carr 8220d1817c working on processing JSON files 2025-09-01 20:14:28 -05:00
Jeff Carr 26d674c800 initial table dumps to STDOUT 2025-09-01 18:57:50 -05:00
Jeff Carr 5dadf3b5ef add a GUI 2025-09-01 13:32:25 -05:00
Jeff Carr cdc4155ef1 ignore json files 2025-09-01 12:38:29 -05:00
Jeff Carr 3a1e76e65e gemini-cli is stupid 2025-09-01 12:23:18 -05:00
Jeff Carr c84460eb65 it builds 2025-09-01 03:37:10 -05:00
Jeff Carr c65619154e sub in the functions 2025-09-01 01:55:06 -05:00
Jeff Carr c63a746d0b isolate this function 2025-09-01 01:41:03 -05:00
Jeff Carr c110250dc2 something? 2025-09-01 00:35:05 -05:00
Jeff Carr 4a800a7cfd attempts to submit data to the Gemini API 2025-09-01 00:29:48 -05:00
Castor Regex eba85c6b97 feat: dump last chat entry 2025-08-31 22:34:53 -05:00
Castor Regex 710693b18a feat: add dumpchat function 2025-08-31 22:31:11 -05:00
Castor Regex 8c8fccc650 feat: print last playback UUID 2025-08-31 22:26:49 -05:00
Jeff Carr 535cb98744 looking good. adds the json file okay 2025-08-30 19:08:41 -05:00
Jeff Carr 6ce2991074 more crap 2025-08-30 18:48:44 -05:00
Jeff Carr 96843095f5 minor 2025-08-30 18:14:47 -05:00
Jeff Carr fbbed0475b parse stuff 2025-08-30 18:07:41 -05:00
Jeff Carr 512ebf5be6 attempt to marshal JSON file to protobuf 2025-08-30 16:54:54 -05:00
Jeff Carr 6888512b3b minor cleanups 2025-08-30 16:17:01 -05:00
Jeff Carr b750fb9252 GEMINI workflow rules 2025-08-30 15:48:22 -05:00
Jeff Carr bbb12d79e2 more code cleanups 2025-08-30 15:24:54 -05:00
Jeff Carr c7896e47f9 code cleanup of early drafts of code from Gemini AI 2025-08-30 15:15:43 -05:00
Jeff Carr 7f8f5e3b9b basic JSON parsing into genai struc 2025-08-30 14:42:57 -05:00
Jeff Carr c971bfddf7 minor 2025-08-28 11:30:13 -05:00
36 changed files with 1456 additions and 321 deletions

4
.gitignore vendored
View File

@ -3,4 +3,6 @@ go.mod
go.sum
/resources/*.so
/files/*
gemini
regex
/tmp/*
*.json

17
GEMINI.md Normal file
View File

@ -0,0 +1,17 @@
## Gemini Added Memories
- The user and I were analyzing a RISC-V Go panic. The user provided a panic log showing a 'SIGQUIT: quit' signal. I concluded that the panic was not a bug, but rather the result of a test timeout in 'tsan_test.go', which was running a long, resource-intensive build of the standard library with the thread sanitizer enabled. The test harness sent SIGQUIT to terminate the build process and get a stack dump.
- Assume my base directory is `/home/jcarr/go/src`
- The gemini-cli application is located in github.com/google-gemini/gemini-cli and is a Node.js project.
- To test the build of gemini-cli, always run exactly: cd github.com/google-gemini/gemini-cli && make build
- The "regex" application is located in `go.wit.com/apps/regex` and is written in Go
- To build the 'regex' application, I must first change into the `go.wit.com/apps/regex` directory and then run the `make regex` command.
- The "Google Gemini AI GO API" is located in `google.golang.org/genai`.
- I must not search for anything. Instead, I must stop and ask the user for the location of files or information.
- When committing to git, I will use the author name 'Castor Regex' and the email 'regex@wit.com'.
- My git workflow is: 1. `git status`. 2. `git add <files>`. 3. `GIT_AUTHOR_NAME='Castor Regex' GIT_AUTHOR_EMAIL='regex@wit.com' git commit -m '...'`. I will not run `git push`.
- When asked to commit code with git, I should follow the git workflow
- My standard workflow: Upon completing a task and verifying the solution, I will automatically commit the changes. Then follow my git workflow
- My standard git workflow: Upon completing a task and verifying the solution, I will automatically commit the changes following the standard workflow.
- New operational rule: If a fix or modification I make results in an error, I will NOT revert the changes with `git checkout`. I will leave the modified files in their current state for the user to inspect and debug.
- CRITICAL WORKFLOW RULE: After making any code changes and before committing, I MUST always attempt to build the project (e.g., run 'make' or the appropriate build command). I am only allowed to commit changes that compile successfully. If a build fails, I must fix the compilation error before proceeding with the commit. I will not commit broken code.
- WORKFLOW UPDATE: If a build fails and the user subsequently tells me to commit, I will assume the user has fixed the compilation errors. My next step is to re-run the build command to verify. If this verification build succeeds, I will then proceed with committing my original changes. If it still fails, I will report the new error.

View File

@ -1,7 +1,8 @@
VERSION = $(shell git describe --tags)
BUILDTIME = $(shell date +%Y.%m.%d_%H%M)
default: verbose
default: install
regex clean
vet:
@GO111MODULE=off go vet
@ -15,30 +16,30 @@ build: goimports
GO111MODULE=off go build \
-ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}"
install: goimports vet
install: goimports
GO111MODULE=off go install \
-ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}"
regex:
regex: goimports
GO111MODULE=off go install \
-ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}"
CUI: install
regex --gui gocui
GTK: clean install
regex --gui andlabs
dumb-build:
go install \
go install -v -x \
-ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}"
install-raw: goimports vet
go install \
-ldflags "-X main.VERSION=${VERSION} -X main.BUILDTIME=${BUILDTIME} -X gui.GUIVERSION=${VERSION}"
andlabs: clean install
regex --gui gocui --gui-verbose --gui-file ../../toolkits/andlabs/andlabs.so
gocui: install
regex --gui gocui --gui-verbose --gui-file ../../toolkits/gocui/gocui.so >/tmp/regex.log 2>&1
goimports:
reset
goimports -w *.go
@# // to globally reset paths:
@# // gofmt -w -r '"go.wit.com/gui/gadgets" -> "go.wit.com/lib/gadgets"' *.go
@ -55,3 +56,5 @@ playback:
regex playback
# regex playback --uuid a1b2c3d4-e5f6-4a5b-8c9d-1e2f3a4b5c6d
tmpfiles:
ls -tl /tmp/regex.* |head

44
README.md Normal file
View File

@ -0,0 +1,44 @@
Gemini is the engine. Vertex AI is the entire car.
* Gemini is the name of the powerful, multimodal AI model family itself. It's the "brain" that performs the reasoning, understands text, images, audio, and video, and
generates responses.
* Vertex AI is the comprehensive, enterprise-grade platform on Google Cloud where you can access, deploy, manage, and even customize models like Gemini. It's the
infrastructure, the dashboard, the security features, and the MLOps (Machine Learning Operations) toolkit that surrounds the engine.
Here is a more detailed breakdown:
Gemini AI
* What it is: A family of highly capable large language models (LLMs).
* What it does: It's the core technology that processes information and generates output. It comes in different sizes and capabilities, like Gemini 1.5 Pro, Gemini
1.5 Flash, and Gemini Ultra, each optimized for different tasks (speed, cost, power).
* Key Features:
* Multimodality: Natively understands and reasons across text, code, images, audio, and video.
* Long Context: Can process massive amounts of information at once (e.g., Gemini 1.5 Pro has a 1 million token context window).
* Advanced Reasoning: Capable of complex, multi-step reasoning tasks.
* How you access it: You access Gemini through an API. That API can be part of Vertex AI or part of a simpler platform like Google AI Studio.
Vertex AI
* What it is: A fully-managed Machine Learning (ML) platform on Google Cloud.
* What it does: It provides all the tools and infrastructure needed to build, deploy, and manage ML models in a production environment. It's not just for Gemini; you
can use it for custom models built with TensorFlow or PyTorch, or other foundation models.
* Key Features:
* Model Garden: A central place to discover and use Google's foundation models (like Gemini) and hundreds of open-source models.
* Enterprise Security & Governance: Integrates with Google Cloud's robust security features like IAM (Identity and Access Management), VPC Service Controls, and
data encryption. Your data remains within your cloud environment.
* Data Integration: Seamlessly connects to other Google Cloud services like BigQuery and Cloud Storage, allowing you to use your own data with the models.
* Tuning and Customization: Provides tools to fine-tune foundation models like Gemini on your own data to make them experts in your specific domain.
* MLOps: A full suite of tools for automating, monitoring, and managing the entire ML lifecycle (pipelines, versioning, etc.).
How They Work Together
You don't choose between Vertex AI and Gemini. You choose how you want to use Gemini, and Vertex AI is the professional, enterprise-grade way to do it.
* When you call the Gemini API via Vertex AI, you get all the benefits of the Google Cloud platform: security, data privacy, scalability, and integration with your
other cloud resources. This is the path for building production applications.
* There is another way to access Gemini: Google AI Studio. This is a web-based tool designed for rapid prototyping and experimentation. It's great for developers who
want to quickly try out prompts and build an API key, but it doesn't have the enterprise-level MLOps and governance features of Vertex AI.
Summary Table

11
add.go
View File

@ -1,7 +1,6 @@
package main
import (
"fmt"
"os"
"path/filepath"
@ -14,15 +13,15 @@ import (
func addFile(filename string) (*chatpb.Chats, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", filename, err)
return nil, log.Errorf("failed to read file %s: %w", filename, err)
}
logData, err := chatpb.UnmarshalChatsTEXT(data)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal log file %s: %w", filename, err)
return nil, log.Errorf("failed to unmarshal log file %s: %w", filename, err)
}
log.Infof("Successfully parsed log file: %s", filename)
log.Info("Successfully parsed log file: %s", filename)
// Get the directory of the log file to resolve relative content paths.
logDir := filepath.Dir(filename)
@ -35,7 +34,7 @@ func addFile(filename string) (*chatpb.Chats, error) {
contentPath := filepath.Join(logDir, contentFile)
contentBytes, err := os.ReadFile(contentPath)
if err != nil {
entry.Content = fmt.Sprintf("--- ERROR: Could not read content file %s: %v ---", contentPath, err)
entry.Content = log.Sprintf("--- ERROR: Could not read content file %s: %v ---", contentPath, err)
} else {
entry.Content = string(contentBytes)
}
@ -48,7 +47,7 @@ func addFile(filename string) (*chatpb.Chats, error) {
snippetPath := filepath.Join(logDir, snippetFile)
contentBytes, err := os.ReadFile(snippetPath)
if err != nil {
snippet.Content = fmt.Sprintf("--- ERROR: Could not read snippet file %s: %v ---", snippetPath, err)
snippet.Content = log.Sprintf("--- ERROR: Could not read snippet file %s: %v ---", snippetPath, err)
} else {
snippet.Content = string(contentBytes)
}

13
addResponse.go Normal file
View File

@ -0,0 +1,13 @@
package main
import (
"go.wit.com/log"
"google.golang.org/genai"
)
func addResponse(resp *genai.GenerateContentResponse) {
pb := convertToPB(resp)
log.Info(resp)
me.lastChat.AppendEntry(pb)
me.chats.ConfigSave()
}

65
apiExampleCode.go Normal file
View File

@ -0,0 +1,65 @@
package main
// this is just example code the GO API's wrapper for handling statelessness
// it doesn't really compile and is just junk Gemini AI sent back but I saved it here anyway
/*
func statelessnessExample() {
ctx := context.Background()
// Get the API key from an environment variable
apiKey := os.Getenv("GEMINI_API_KEY")
if apiKey == "" {
log.Fatal("GEMINI_API_KEY environment variable not set")
}
// Create a new client
client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Choose the model
model := client.GenerativeModel("gemini-1.5-flash")
// ---- Start a new chat session ----
cs := model.StartChat()
cs.History = []*genai.Content{} // Start with a clean history
// --- First message ---
fmt.Println("User: My brother's name is Paul.")
resp, err := cs.SendMessage(ctx, genai.Text("My brother's name is Paul."))
if err != nil {
log.Fatal(err)
}
printResponse(resp)
// --- Second message ---
// The ChatSession now remembers the previous exchange.
fmt.Println("\nUser: What is my brother's name?")
resp, err = cs.SendMessage(ctx, genai.Text("What is my brother's name?"))
if err != nil {
log.Fatal(err)
}
printResponse(resp)
// You can inspect the history at any time
// fmt.Println("\n--- Full Chat History ---")
// for _, content := range cs.History {
// for _, part := range content.Parts {
// fmt.Printf("Role: %s, Text: %v\n", content.Role, part)
// }
// }
}
// Helper function to print the response
func printResponse(resp *genai.GenerateContentResponse) {
for _, cand := range resp.Candidates {
if cand.Content != nil {
for _, part := range cand.Content.Parts {
fmt.Printf("Gemini: %v\n", part)
}
}
}
}
*/

38
argv.go
View File

@ -10,29 +10,33 @@ package main
var argv args
type args struct {
Add string `arg:"--add" help:"add a new chat"`
Format *EmptyCmd `arg:"subcommand:format" help:"add a conversation"`
Playback *PlaybackCmd `arg:"subcommand:playback" help:"dump your prior conversations to the terminal'"`
Output string `arg:"--output" help:"should get a string from regex-cli"`
Input string `arg:"--input" help:"should get a string from regex-cli"`
Editor *EmptyCmd `arg:"subcommand:editor" help:"open env EDITOR"`
ImportFile string `arg:"--import" help:"import a file from regex-cli"`
Stats []string `arg:"--stats" help:"add stats to a chat"`
NewChat []string `arg:"--new-chat" help:"create a new chat"`
GetNextAutoTopic bool `arg:"--get-next-auto-topic" help:"get the next auto topic name"`
Force bool `arg:"--force" help:"try to strong arm things"`
Verbose bool `arg:"--verbose" help:"show more output"`
Bash bool `arg:"--bash" help:"generate bash completion"`
BashAuto []string `arg:"--auto-complete" help:"todo: move this to go-arg"`
Uuid string `arg:"--uuid" help:"look at this uuid"`
Topic string `arg:"--topic" help:"set the topic"`
JsonFile string `arg:"--json" help:"import a JSON file from gemini-cli"`
Interact *EmptyCmd `arg:"subcommand:interact" help:"open env EDITOR"`
Playback *PlaybackCmd `arg:"subcommand:playback" help:"dump your prior conversations to the terminal'"`
NewChat *PlaybackCmd `arg:"subcommand:newchat" help:"used by gemini-cli on startup"`
Clean *CleanCmd `arg:"subcommand:clean" help:"cleanup the files in /tmp"`
Stats string `arg:"--stats" help:"add stats to a chat"`
Force bool `arg:"--force" help:"try to strong arm things"`
Verbose bool `arg:"--verbose" help:"show more output"`
Bash bool `arg:"--bash" help:"generate bash completion"`
BashAuto []string `arg:"--auto-complete" help:"todo: move this to go-arg"`
}
type EmptyCmd struct {
}
type CleanCmd struct {
Match string `arg:"positional"`
}
type PlaybackCmd struct {
List *EmptyCmd `arg:"subcommand:list" help:"list memories"`
Long *EmptyCmd `arg:"subcommand:long" help:"show info on each chat"`
Uuid string `arg:"--uuid" help:"look at this uuid"`
Last *EmptyCmd `arg:"subcommand:last" help:"dump the last one"`
List *EmptyCmd `arg:"subcommand:list" help:"list memories"`
Long *EmptyCmd `arg:"subcommand:long" help:"show info on each chat"`
Purge *EmptyCmd `arg:"subcommand:purge" help:"verify chat uuids & purge empty chats"`
Submit *EmptyCmd `arg:"subcommand:submit" help:"convert and submit the last entry"`
}
func (args) Version() string {

View File

@ -21,16 +21,16 @@ func deleteMatch() {
}
func (args) doBashAuto() {
argv.doBashHelp()
// argv.doBashHelp()
switch argv.BashAuto[0] {
case "playback":
fmt.Println("long --uuid")
fmt.Println("long --uuid purge last submit")
case "clean":
fmt.Println("user devel master")
fmt.Println("")
default:
if argv.BashAuto[0] == ARGNAME {
// list the subcommands here
fmt.Println("--add format playback editor")
fmt.Println("--json interact playback clean")
}
}
os.Exit(0)

26
convertToGenai.go Normal file
View File

@ -0,0 +1,26 @@
package main
import (
"go.wit.com/lib/protobuf/chatpb"
"google.golang.org/genai"
)
// convertToGenai transforms the parsed JSON request into the genai.Content format.
func convertToGenai(req *chatpb.GeminiRequest) ([]*genai.Content, error) {
var contents []*genai.Content
for _, c := range req.GetContents() {
var genaiParts []*genai.Part
for _, p := range c.GetParts() {
switch v := p.GetPartType().(type) {
case *chatpb.Part_Text:
part := &genai.Part{Text: v.Text}
genaiParts = append(genaiParts, part)
}
}
contents = append(contents, &genai.Content{
Role: c.GetRole(),
Parts: genaiParts,
})
}
return contents, nil
}

48
convertToPB.go Normal file
View File

@ -0,0 +1,48 @@
package main
import (
"fmt"
"go.wit.com/lib/protobuf/chatpb"
"google.golang.org/genai"
)
func convertToPB(resp *genai.GenerateContentResponse) *chatpb.ChatEntry {
entry := &chatpb.ChatEntry{}
if resp == nil || len(resp.Candidates) == 0 || resp.Candidates[0].Content == nil {
return entry
}
content := resp.Candidates[0].Content
for _, part := range content.Parts {
if part.Text != "" {
entry.Parts = append(entry.Parts, &chatpb.Part{
PartType: &chatpb.Part_Text{Text: part.Text},
})
}
if fc := part.FunctionCall; fc != nil {
fmt.Printf("Gemini API requested to execute command: %s\n", fc.Name)
entry.Parts = append(entry.Parts, &chatpb.Part{
PartType: &chatpb.Part_FunctionCall{
FunctionCall: &chatpb.FunctionCall{
Name: fc.Name,
Args: &chatpb.ArgsInfo{}, // TODO: Properly map args from fc.Args map[string]any
},
},
})
}
if fr := part.FunctionResponse; fr != nil {
// Convert the FunctionResponse to the protobuf equivalent
entry.Parts = append(entry.Parts, &chatpb.Part{
PartType: &chatpb.Part_FunctionResponse{
FunctionResponse: &chatpb.FunctionResponse{
Name: fr.Name,
// TODO: Properly map the response content
},
},
})
}
}
return entry
}

20
debugger.go Normal file
View File

@ -0,0 +1,20 @@
package main
/*
enables GUI options and the debugger in your application
*/
import (
"go.wit.com/lib/debugger"
"go.wit.com/log"
)
func init() {
if debugger.ArgDebug() {
log.Info("cmd line --debugger == true")
go func() {
log.Sleep(2)
debugger.DebugWindow()
}()
}
}

129
doClean.go Normal file
View File

@ -0,0 +1,129 @@
package main
import (
"os"
"path/filepath"
"strings"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
"google.golang.org/protobuf/types/known/timestamppb"
)
func doClean() {
log.Info("find all files")
scanTmp()
}
func scanTmp() {
var count int
filepath.WalkDir("/tmp", func(path string, d os.DirEntry, err error) error {
if err != nil {
// Handle possible errors, like permission issues
// fmt.Fprintf(os.Stderr, "error accessing path %q: %v\n", path, err)
// ignore all these problems
return err
}
/*
if d.IsDir() {
// log.Info("path is dir", path)
return nil
}
*/
_, fname := filepath.Split(path)
if !strings.HasPrefix(fname, "regex.") {
return nil
}
if count > 100 {
return log.Errorf("count exceeded")
}
if strings.HasPrefix(fname, "regex.gemini-api-response") {
// log.Info("response file:", fname)
return nil
}
if strings.Contains(fname, "gemini-api-request") {
// log.Info("response file:", fname)
if err := cleanGeminiFile(path); err == nil {
count += 1
return nil
} else {
return nil
}
}
if strings.HasSuffix(fname, ".stats") {
cleanStatsFile(path)
return nil
}
log.Info("check file:", path)
return nil
})
}
func cleanStatsFile(fullname string) {
log.Info("stats file", fullname)
}
func cleanGeminiFile(fullname string) error {
_, fname := filepath.Split(fullname)
if !strings.HasSuffix(fname, ".json") {
return log.Errorf("not really gemini-api-request .json")
}
parts := strings.Split(fname, ".")
if len(parts) == 5 {
if parts[2] != "gemini-api-request" {
return log.Errorf("not really gemini-api-request")
}
}
// log.Info("PARSE FILE:", fullname)
pb, err := parsePB(fullname)
if err != nil {
log.Info("parsePB() %s err %v\n", fullname, err)
return log.Errorf("parsePB() %s err %v", fullname, err)
}
if pb == nil {
log.Info("parsePB() == nil\n")
return log.Errorf("parsePB() == nil")
}
uuid := parts[1]
if argv.Clean != nil && argv.Clean.Match != "" {
if !strings.HasPrefix(uuid, argv.Clean.Match) {
return log.Errorf("uuid %s does not match %s", uuid, argv.Clean.Match)
}
}
if chat := me.chats.FindUuid(uuid); chat != nil {
log.Info("found uuid in chat", uuid, pb.Model, chat.Uuid)
statf, err := os.Stat(fullname)
if err == nil {
age := statf.ModTime()
if chat.AddGeminiRequest(fname, age, pb) {
me.chats.ConfigSave()
} else {
log.Info("file was perfect. os.Remove() here", fullname)
chat.PrintChatGeminiTable()
os.Remove(fullname)
}
}
return nil
}
id := parts[3]
if id == "1" {
log.Info("Insert new chat here", fullname, id)
statf, err := os.Stat(fullname)
if err == nil {
age := statf.ModTime()
c := new(chatpb.Chat)
c.Ctime = timestamppb.New(age)
c.Uuid = uuid
c.ChatName = "auto clean"
log.Info("new chat:", c.Uuid, c.Ctime)
me.chats.Append(c)
me.chats.ConfigSave()
okExit("")
}
}
log.Info("gemini JSON file uuid not found", fullname, id)
return log.Errorf("gemini JSON file uuid %s not found", uuid)
}

136
doConnect.go Normal file
View File

@ -0,0 +1,136 @@
package main
import (
"context"
"fmt"
"os"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
"google.golang.org/genai"
)
func initGeminiAPI() error {
if me.ctx != nil {
// already initialized
return nil
}
apiKey := os.Getenv("GEMINI_API_KEY")
if apiKey == "" {
return log.Errorf("GEMINI_API_KEY environment variable not set")
}
me.ctx = context.Background()
var err error
me.client, err = genai.NewClient(me.ctx, &genai.ClientConfig{APIKey: apiKey})
if err != nil {
return log.Errorf("failed to create new genai client: %w", err)
}
return nil
}
// doConnect initializes the Gemini client and handles the request flow.
func doConnect() error {
initGeminiAPI()
if me.lastChat == nil {
log.Info("WTF. lastChat is nil")
return nil
}
// if me.lastChat.Entries == nil {
// me.lastChat.Entries = new(chatpb.ChatEntry)
// }
// In a real application, you would get user input here.
// For now, we'll use a hardcoded prompt.
if len(me.lastChat.GetEntries()) == 0 {
me.lastChat.Entries = append(me.lastChat.Entries, &chatpb.ChatEntry{
Parts: []*chatpb.Part{
{PartType: &chatpb.Part_Text{Text: "hello, how are you"}},
},
})
}
lastEntry := me.lastChat.GetEntries()[len(me.lastChat.GetEntries())-1]
genaiContents, err := convertToGenai(lastEntry.GetGeminiRequest())
if err != nil {
return err
}
resp, err := me.client.Models.GenerateContent(me.ctx, "gemini-2.5-flash", genaiContents, nil)
if err != nil {
return log.Errorf("error sending message: %v", err)
}
if resp == nil || len(resp.Candidates) == 0 || resp.Candidates[0].Content == nil {
log.Info("Received an empty response from the API. Stopping.")
return nil
}
// Append the model's response to the history
me.lastChat.Entries = append(me.lastChat.Entries, convertToPB(resp))
// Check for a function call
hasFunctionCall := false
for _, part := range resp.Candidates[0].Content.Parts {
if fc := part.FunctionCall; fc != nil {
hasFunctionCall = true
functionResponse := handleFunctionCall(fc)
// Append the function response to the history for the next turn
me.lastChat.Entries = append(me.lastChat.Entries, &chatpb.ChatEntry{
Parts: []*chatpb.Part{
{PartType: &chatpb.Part_FunctionResponse{
FunctionResponse: &chatpb.FunctionResponse{
Name: functionResponse.Name,
// TODO: map response
},
}},
},
})
}
}
// If there was no function call, print the text and stop.
if !hasFunctionCall {
log.Info("Response from API:")
for _, cand := range resp.Candidates {
if cand.Content != nil {
for _, part := range cand.Content.Parts {
if part.Text != "" {
fmt.Println(part.Text)
}
}
}
}
}
return nil
}
// sampleHello sends a hardcoded prompt to the model and prints the response.
func simpleHello() error {
log.Info("Sending 'hello, how are you' to the Gemini API...")
// Create the parts slice
parts := []*genai.Part{
{Text: "What is my brothers name?"},
}
content := []*genai.Content{{Parts: parts}}
resp, err := me.client.Models.GenerateContent(me.ctx, "gemini-2.5-flash", content, nil)
if err != nil {
return log.Errorf("error sending message: %v", err)
}
log.Info("Response from API:")
for _, cand := range resp.Candidates {
if cand.Content != nil {
for _, part := range cand.Content.Parts {
fmt.Println(part)
}
}
}
return nil
}

View File

@ -1,28 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"github.com/google/generative-ai-go/genai"
"google.golang.org/api/option"
)
// doConnect initializes and returns a Gemini client.
// It reads the API key from the GEMINI_API_KEY environment variable.
func doConnect() (*genai.GenerativeModel, error) {
apiKey := os.Getenv("GEMINI_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("GEMINI_API_KEY environment variable not set")
}
ctx := context.Background()
client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
if err != nil {
return nil, fmt.Errorf("failed to create new genai client: %w", err)
}
model := client.GenerativeModel("gemini-pro")
return model, nil
}

104
doGui.go Normal file
View File

@ -0,0 +1,104 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
// An app to submit patches for the 30 GO GUI repos
import (
"fmt"
"os"
"time"
"go.wit.com/gui"
"go.wit.com/lib/debugger"
"go.wit.com/lib/gadgets"
"go.wit.com/lib/gui/logsettings"
"go.wit.com/lib/gui/shell"
"go.wit.com/log"
)
func debug() {
time.Sleep(2 * time.Second)
for {
log.Info("idle loop() todo: could check for things here")
time.Sleep(90 * time.Second)
}
}
func doGui() {
me.myGui = gui.New()
// me.myGui.SetAppDefaultPlugin(me.forge.Config.DefaultGui)
me.myGui.Default()
win := gadgets.NewGenericWindow("regex: a WIT Cloud private AI tool", "Current Conversations")
drawWindow(win)
win.Custom = func() {
log.Warn("MAIN WINDOW CLOSE")
me.myGui.StandardExit()
os.Exit(0)
}
me.mainWindow = win
// sits here forever
debug()
}
func drawWindow(win *gadgets.GenericWindow) {
grid := win.Group.RawGrid()
grid.NewLabel("label worked")
grid.NextRow()
var insertWin *gadgets.GenericWindow
s := fmt.Sprintf("Show Chat Entries (%d)", me.chats.Len())
grid.NewButton(s, func() {
// if the window exists, just toggle it open or closed
if insertWin != nil {
insertWin.Toggle()
return
}
insertWin = makeChatsWindow()
})
grid.NewButton("simple hello", func() {
err := simpleHello()
if err != nil {
badExit(err)
}
})
grid.NewButton("submit question", func() {
doEditorOnce()
})
grid.NewButton("print playback", func() {
shell.RunVerbose([]string{"regex", "playback"})
})
grid.NewButton("clean", func() {
doClean()
})
grid.NextRow()
grid.NewButton("debugger", func() {
debugger.DebugWindow()
})
grid.NewButton("logging", func() {
logsettings.LogWindow()
})
}
// old things before they are removed, deprecated, fixed, etc
func makeOldStuff() *gadgets.GenericWindow {
oldWin := gadgets.NewGenericWindow("old code", "old code on it's way out")
grid := oldWin.Group.RawGrid()
grid.NewButton("Release Window", func() {
log.Info("todo: move releaser here")
})
return oldWin
}

View File

@ -1,56 +0,0 @@
package main
import (
"io/ioutil"
"time"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
"google.golang.org/protobuf/types/known/timestamppb"
)
func doImport(filename string) {
content, err := ioutil.ReadFile(filename)
if err != nil {
log.Warn("Error reading import file:", err)
return
}
s := string(content)
// Load the existing chats.
all := chatpb.NewChats()
if err := all.ConfigLoad(); err != nil {
log.Warn("Error loading config, can't add to auto chat:", err)
return
}
// Find the "auto" chat.
var autoChat *chatpb.Chat
for _, chat := range all.GetChats() {
if chat.GetChatName() == "auto" {
autoChat = chat
break
}
}
// If the "auto" chat is found, add the new entry.
if autoChat != nil {
newEntry := &chatpb.ChatEntry{
From: chatpb.Who_REGEX,
Ctime: timestamppb.New(time.Now()),
ToolCalls: []*chatpb.ToolCall{
{
Name: "Shell",
Input: s,
},
},
}
autoChat.Entries = append(autoChat.Entries, newEntry)
if err := all.ConfigSave(); err != nil {
log.Warn("Error saving config after adding to auto chat:", err)
} else {
log.Info("Added new entry to 'auto' chat.")
}
}
}

View File

@ -1,56 +0,0 @@
package main
import (
"os"
"time"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
"google.golang.org/protobuf/types/known/timestamppb"
)
func doInput(s string) {
filename := "/tmp/regex-input.log"
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Println(err)
return
}
defer f.Close()
if _, err := f.WriteString(s + "\n"); err != nil {
log.Println(err)
}
log.Info("INPUT LOGGED TO", filename)
// Load the existing chats.
all := chatpb.NewChats()
if err := all.ConfigLoad(); err != nil {
log.Warn("Error loading config, can't add to auto chat:", err)
return
}
// Find the "auto" chat.
var autoChat *chatpb.Chat
for _, chat := range all.GetChats() {
if chat.GetChatName() == "auto" {
autoChat = chat
break
}
}
// If the "auto" chat is found, add the new entry.
if autoChat != nil {
newEntry := &chatpb.ChatEntry{
From: chatpb.Who_USER,
Content: s,
Ctime: timestamppb.New(time.Now()),
}
autoChat.Entries = append(autoChat.Entries, newEntry)
if err := all.ConfigSave(); err != nil {
log.Warn("Error saving config after adding to auto chat:", err)
} else {
log.Info("Added new entry to 'auto' chat.")
}
}
}

View File

@ -10,7 +10,7 @@ import (
"go.wit.com/log"
)
func doEditor() error {
func doInteract() error {
for {
filename, err := doEditorOnce()
if err != nil {
@ -32,7 +32,7 @@ func doEditor() error {
log.Error(err)
}
os.Remove("/tmp/regex.ready")
log.Infof("SessionID: %s", string(content))
log.Info("SessionID: %s", string(content))
logContent, err := ioutil.ReadFile("/tmp/regex.log")
if err != nil {

43
doJSON.go Normal file
View File

@ -0,0 +1,43 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
// An app to submit patches for the 30 GO GUI repos
import (
"path/filepath"
"strconv"
"strings"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
)
func doJSON() {
// now try to Marshal() into a protobuf
pb, err := parsePB(argv.JsonFile)
if err != nil {
badExit(err)
}
log.Info("GeminiContent pb.Marshal() worked pb.Contents len =", len(pb.Contents))
_, filename := filepath.Split(argv.JsonFile)
parts := strings.Split(filename, ".")
if len(parts) == 5 {
uuid := parts[1]
num, _ := strconv.Atoi(parts[3])
log.Info(uuid, parts)
if chat := me.chats.FindByUuid(uuid); chat != nil {
log.Info("FOUND CHAT", uuid, num)
newEntry := new(chatpb.ChatEntry)
newEntry.GeminiRequest = pb
newEntry.ContentFile = filename
newEntry.RequestCounter = int32(num)
chat.AppendEntry(newEntry)
me.chats.ConfigSave()
}
} else {
}
okExit("")
}

View File

@ -1,28 +1,24 @@
package main
import (
"fmt"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
"google.golang.org/protobuf/types/known/timestamppb"
)
func doNewChat() {
if len(argv.NewChat) != 2 {
log.Error(fmt.Errorf("expected 2 arguments for --new-chat"))
if found := me.chats.FindByUuid(argv.Uuid); found != nil {
found.ChatName = argv.Topic
me.chats.ConfigSave()
return
}
uuid := argv.NewChat[0]
topic := argv.NewChat[1]
chat := &chatpb.Chat{
Uuid: uuid,
ChatName: topic,
Uuid: argv.Uuid,
ChatName: argv.Topic,
Ctime: timestamppb.Now(),
}
me.chats.Chats = append(me.chats.Chats, chat)
me.chats.ConfigSave()
log.Info("created new chat for", uuid)
log.Info("created new chat for", argv.Uuid)
}

View File

@ -1,20 +1,38 @@
package main
import (
"fmt"
"strings"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
)
func doPlayback() {
if argv.Playback.Uuid != "" {
showChat(argv.Playback.Uuid)
return
func doPlayback() error {
if argv.Uuid != "" {
showChat(argv.Uuid)
return nil
}
if argv.Playback.Purge != nil {
doPurge()
return nil
}
listChats(me.chats)
return nil
}
func doPurge() {
changed := false
for _, chat := range me.chats.GetChats() {
if len(chat.GetEntries()) == 0 {
me.chats.Delete(chat)
changed = true
}
}
if changed {
me.chats.ConfigSave()
}
}
func showChat(uuid string) {
@ -34,8 +52,8 @@ func listChats(chats *chatpb.Chats) {
return
}
log.Infof("Found %d chat topic(s) in the log.", len(chats.GetChats()))
fmt.Println("-------------------------------------------------")
log.Info("Found %d chat topic(s) in the log.", len(chats.GetChats()))
log.Println("-------------------------------------------------")
for _, chat := range chats.GetChats() {
entryCount := len(chat.GetEntries())
@ -47,20 +65,28 @@ func listChats(chats *chatpb.Chats) {
formattedTime = "No Timestamp"
}
fmt.Printf("Topic: %-25s | Entries: %-4d | Started: %s | UUID: %s\n",
chat.GetChatName(),
log.Printf("Entries: %-4d | Started: %-25s | UUID: %s | %-25s\n",
entryCount,
formattedTime,
chat.GetUuid(),
chat.GetChatName(),
)
}
fmt.Println("-------------------------------------------------")
log.Println("-------------------------------------------------")
// Get the last chat
numChats := len(chats.GetChats())
if numChats > 0 {
me.lastChat = chats.GetChats()[numChats-1]
log.Printf("The current Gemini API session is UUID: %s\n", me.lastChat.GetUuid())
dumpchat(me.lastChat)
}
}
// print out one line for each chat entry
func listEntries(chat *chatpb.Chat) {
fmt.Printf("--- Entries for Topic: %s ---\n", chat.GetChatName())
width := getTerminalWidth()
log.Printf("--- Entries for Topic: %s ---\n", chat.GetChatName())
width, _ := getTerminalWidth()
// Determine the maximum length of the author and time string
maxAuthorAndTimeLen := 0
@ -73,7 +99,7 @@ func listEntries(chat *chatpb.Chat) {
} else {
formattedTime = "No Time"
}
authorAndTime := fmt.Sprintf("[%s] (%s)", author, formattedTime)
authorAndTime := log.Sprintf("[%s] (%s)", author, formattedTime)
if len(authorAndTime) > maxAuthorAndTimeLen {
maxAuthorAndTimeLen = len(authorAndTime)
}
@ -94,7 +120,7 @@ func listEntries(chat *chatpb.Chat) {
// Replace newlines with spaces for a clean one-line view
contentPreview = strings.ReplaceAll(contentPreview, "\n", " ")
authorAndTime := fmt.Sprintf("[%s] (%s)", author, formattedTime)
authorAndTime := log.Sprintf("[%s] (%s)", author, formattedTime)
availableWidth := width - maxAuthorAndTimeLen - 1 // -1 for a space
if len(contentPreview) > availableWidth {
@ -112,14 +138,14 @@ func listEntries(chat *chatpb.Chat) {
if authorAndTimePadding < 0 {
authorAndTimePadding = 0
}
fmt.Printf("%s%s%s%s\n", contentPreview, strings.Repeat(" ", padding), strings.Repeat(" ", authorAndTimePadding), authorAndTime)
log.Printf("%s%s%s%s\n", contentPreview, strings.Repeat(" ", padding), strings.Repeat(" ", authorAndTimePadding), authorAndTime)
} else {
padding := maxAuthorAndTimeLen - len(authorAndTime)
if padding < 0 {
padding = 0
}
fmt.Printf("%s%s %s\n", authorAndTime, strings.Repeat(" ", padding), contentPreview)
log.Printf("%s%s %s\n", authorAndTime, strings.Repeat(" ", padding), contentPreview)
}
}
fmt.Println("-------------------------------------------------")
log.Println("-------------------------------------------------")
}

View File

@ -2,7 +2,6 @@ package main
import (
"encoding/json"
"fmt"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
@ -10,12 +9,8 @@ import (
)
func doStats() {
if len(argv.Stats) != 2 {
log.Error(fmt.Errorf("expected 2 arguments for --stats"))
return
}
sessionUuid := argv.Stats[0]
statsString := argv.Stats[1]
sessionUuid := argv.Uuid
statsString := "todo: set this somehow"
// Find the "auto" chat, or create it if it doesn't exist.
var autoChat *chatpb.Chat
@ -36,7 +31,7 @@ func doStats() {
var stats chatpb.SessionStats
err := json.Unmarshal([]byte(statsString), &stats)
if err != nil {
log.Error(fmt.Errorf("error unmarshalling stats: %w", err))
log.Printf("error unmarshalling stats: %w", err)
return
}

39
dumpchat.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
)
func dumpEntry(entry *chatpb.ChatEntry) {
log.Printf(entry.FormatTEXT())
}
func dumpchat(chat *chatpb.Chat) error {
entries := chat.GetEntries()
var lastEntry *chatpb.ChatEntry
if len(entries) > 0 {
lastEntry = entries[len(entries)-1]
} else {
return log.Errorf("no entries")
}
if argv.Playback.Last != nil {
dumpEntry(lastEntry)
}
if argv.Playback.Submit != nil {
if lastEntry.GeminiRequest == nil {
return log.Errorf("lastEntry.GeminiRequest == nil")
}
aichat, err := convertToGenai(lastEntry.GetGeminiRequest())
if err != nil {
log.Info("convertToGenai() returned with error", err)
return err
}
err = submitChat(aichat)
if err != nil {
log.Info("submitChat() returned with error", err)
return err
}
}
return nil
}

View File

@ -1,15 +1,13 @@
package main
import (
"fmt"
"strconv"
"strings"
"go.wit.com/log"
)
func doGetNextAutoTopic() {
if err := me.chats.ConfigLoad(); err != nil {
badExit(err)
}
func getNextAutoTopic() {
max := 0
for _, chat := range me.chats.GetChats() {
if strings.HasPrefix(chat.GetChatName(), "Auto ") {
@ -20,5 +18,5 @@ func doGetNextAutoTopic() {
}
}
}
fmt.Printf("Auto %d", max+1)
log.Printf("Auto %d", max+1)
}

51
handleFunctionCall.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"fmt"
"go.wit.com/log"
"google.golang.org/genai"
)
// handleFunctionCall executes a command requested by the Gemini API and returns the result.
func handleFunctionCall(fc *genai.FunctionCall) *genai.FunctionResponse {
if fc == nil {
return nil
}
if fc.Name != "run_shell_command" {
log.Infof("Unsupported function call: %s", fc.Name)
return &genai.FunctionResponse{
Name: fc.Name,
Response: map[string]any{
"error": fmt.Sprintf("Unsupported function call: %s", fc.Name),
},
}
}
// Extract arguments
cmd, _ := fc.Args["command"].(string)
dir, _ := fc.Args["directory"].(string)
if cmd == "" {
return &genai.FunctionResponse{
Name: fc.Name,
Response: map[string]any{
"error": "missing command argument",
},
}
}
// Execute the command (this is a placeholder for the actual execution)
// In a real implementation, you would use the run_shell_command tool here.
log.Infof("Executing command: '%s' in directory: '%s'", cmd, dir)
// For now, we'll return a dummy response.
// TODO: Replace this with actual command execution.
return &genai.FunctionResponse{
Name: fc.Name,
Response: map[string]any{
"output": "command executed successfully (dummy response)",
},
}
}

125
json.go Normal file
View File

@ -0,0 +1,125 @@
package main
import (
"encoding/json"
"os"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
)
// GeminiRequest matches the overall structure of the gemini-cli JSON output.
type GeminiRequest struct {
Model string `json:"model"`
Contents []Content `json:"contents"`
// Config is left as a raw message because its structure is complex and not needed for now.
Config json.RawMessage `json:"config"`
}
// Content matches the 'contents' array elements.
type Content struct {
Role string `json:"role"`
Parts []Part `json:"parts"`
}
// Part matches the 'parts' array elements.
// It can contain one of several types of data.
type Part struct {
Text string `json:"text,omitempty"`
ThoughtSignature string `json:"thoughtSignature,omitempty"`
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"`
}
// FunctionCall matches the 'functionCall' object.
type FunctionCall struct {
Name string `json:"name"`
Args map[string]string `json:"args"`
}
// FunctionResponse matches the 'functionResponse' object.
type FunctionResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Response map[string]interface{} `json:"response"`
}
func parsePB(filename string) (*chatpb.GeminiRequest, error) {
// Read the entire file
data, err := os.ReadFile(filename)
if err != nil {
return nil, log.Errorf("failed to read file %s: %w", filename, err)
}
pb := new(chatpb.GeminiRequest)
if err := pb.UnmarshalJSON(data); err != nil {
return nil, err
}
return pb, nil
}
// parseJSON opens the given file, reads it, and unmarshals it into our structs.
func parseJSON(filename string) (*GeminiRequest, error) {
log.Infof("Attempting to parse file: %s\n", filename)
// Read the entire file
data, err := os.ReadFile(filename)
if err != nil {
return nil, log.Errorf("failed to read file %s: %w", filename, err)
}
// Unmarshal the JSON data
var req *GeminiRequest
req = new(GeminiRequest)
if err := json.Unmarshal(data, &req); err != nil {
return nil, log.Errorf("failed to unmarshal JSON from %s: %w", filename, err)
}
dumpSummaryJSON(req)
return req, nil
}
func dumpSummaryJSON(req *GeminiRequest) {
var totalFC, totalTexts, totalFR int
// Log the parsed data to confirm it worked
// Example of accessing deeper data
for _, content := range req.Contents {
// log.Infof("Content[%d] Role: %s", i, content.Role)
for _, part := range content.Parts {
if part.Text != "" {
// log.Infof(" Part[%d] Text: %.60s...", j, part.Text) // Print snippet
totalTexts += 1
}
if part.FunctionCall != nil {
// log.Infof(" Part[%d] FunctionCall: %s", j, part.FunctionCall.Name)
totalFC += 1
}
if part.FunctionResponse != nil {
// log.Infof(" Part[%d] FunctionCall: %s", j, part.FunctionCall.Name)
totalFR += 1
}
}
}
log.Printf("Parsed JSON (Model: %s) (# of content blocks %d) (Text #=%d) (FC=%d) (FR=%d)\n", req.Model, len(req.Contents), totalTexts, totalFC, totalFR)
}
func dumpFullJSON(req *GeminiRequest) {
// Log the parsed data to confirm it worked
log.Info("Successfully parsed JSON file.")
log.Infof("Model: %s", req.Model)
log.Infof("Number of content blocks: %d", len(req.Contents))
// Example of accessing deeper data
for i, content := range req.Contents {
log.Infof("Content[%d] Role: %s", i, content.Role)
for j, part := range content.Parts {
if part.Text != "" {
log.Infof(" Part[%d] Text: %.60s...", j, part.Text) // Print snippet
}
if part.FunctionCall != nil {
log.Infof(" Part[%d] FunctionCall: %s", j, part.FunctionCall.Name)
}
}
}
}

92
main.go
View File

@ -32,6 +32,7 @@ var ARGNAME string = "regex"
var configSave bool
func main() {
var err error
me = new(mainType)
gui.InitArg()
me.pp = arg.MustParse(&argv)
@ -45,21 +46,47 @@ func main() {
os.Exit(0)
}
// load the default chat protobuf
me.chats = chatpb.NewChats()
if err := me.chats.ConfigLoad(); err != nil {
badExit(err)
}
// verify all the chats have Uuid's
if verifyUuids(me.chats) {
me.chats.ConfigSave()
}
if argv.GetNextAutoTopic {
doGetNextAutoTopic()
// Get the last chat
numChats := len(me.chats.GetChats())
if numChats > 0 {
me.lastChat = me.chats.GetChats()[numChats-1]
log.Printf("The current Gemini API session is UUID: %s\n", me.lastChat.GetUuid())
}
err = initGeminiAPI()
if err != nil {
badExit(err)
}
if argv.JsonFile != "" {
doJSON()
okExit("")
}
if argv.Editor != nil {
doEditor()
if argv.Interact != nil {
log.Info("testing AI client with simpleHello()")
err = simpleHello()
if err != nil {
badExit(err)
}
doInteract()
okExit("")
}
if argv.Stats != "" {
doStats()
okExit("")
}
@ -68,61 +95,24 @@ func main() {
okExit("")
}
if argv.Stats != nil {
doStats()
okExit("")
}
if argv.Output != "" {
doOutput(argv.Output)
okExit("")
}
if argv.Input != "" {
doInput(argv.Input)
okExit("")
}
if argv.ImportFile != "" {
doImport(argv.ImportFile)
okExit("")
}
if argv.Add != "" {
newChats, err := addFile(argv.Add)
if err != nil {
badExit(err)
}
verifyUuids(newChats)
for _, newChat := range newChats.GetChats() {
me.chats.AppendByUuid(newChat)
log.Info("Attempting to add chat", newChat.ChatName)
}
me.chats.ConfigSave()
okExit("")
}
if argv.Playback != nil {
if argv.Playback.Uuid != "" {
showChat(argv.Playback.Uuid)
if argv.Uuid != "" {
showChat(argv.Uuid)
} else {
doPlayback()
}
okExit("")
}
// if opening the GUI, always check git for dirty repos
log.Info("look for 'auto' here")
// Find the "auto" chat.
for _, chat := range me.chats.GetChats() {
if chat.GetChatName() == "auto" {
prettyFormatChat(chat)
okExit("")
}
if argv.Clean != nil {
doClean()
okExit("")
}
// doGui()
doGui()
// by default, start interacting with gemini-cli
// me.pp.WriteHelp(os.Stdout)
okExit("")
}

View File

@ -7,20 +7,20 @@
package main
import (
"fmt"
"path/filepath"
"strings"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
)
const termWidth = 100 // The target width for the formatted output boxes.
// prettyFormatChat is the main entry point to print a detailed view of a Chat topic.
func prettyFormatChat(chat *chatpb.Chat) {
fmt.Printf("\n========================================================\n")
fmt.Printf("== Chat Topic: %s (UUID: %s)\n", chat.GetChatName(), chat.GetUuid())
fmt.Printf("========================================================\n\n")
log.Printf("\n========================================================\n")
log.Printf("== Chat Topic: %s (UUID: %s)\n", chat.GetChatName(), chat.GetUuid())
log.Printf("========================================================\n\n")
for _, entry := range chat.GetEntries() {
author := entry.GetFrom().String()
@ -50,7 +50,7 @@ func prettyFormatChat(chat *chatpb.Chat) {
printCodeSnippet(snippet)
}
}
fmt.Println()
log.Println()
}
}
@ -65,8 +65,8 @@ func printContent(author, timestamp, content string) {
}
func printLeftAligned(author, timestamp, content string) {
prefix := fmt.Sprintf("✦ %s (%s):", author, timestamp)
fmt.Println(prefix)
prefix := log.Sprintf("✦ %s (%s):", author, timestamp)
log.Println(prefix)
indent := "\t"
contentWidth := termWidth - 8 // 8 spaces for a standard tab
@ -76,7 +76,7 @@ func printLeftAligned(author, timestamp, content string) {
for _, paragraph := range paragraphs {
words := strings.Fields(paragraph)
if len(words) == 0 {
fmt.Println() // Preserve paragraph breaks
log.Println() // Preserve paragraph breaks
continue
}
@ -85,19 +85,19 @@ func printLeftAligned(author, timestamp, content string) {
if len(currentLine)+1+len(word) <= contentWidth {
currentLine += " " + word
} else {
fmt.Println(currentLine)
log.Println(currentLine)
currentLine = indent + word
}
}
fmt.Println(currentLine)
log.Println(currentLine)
}
}
func printRightAligned(author, timestamp, content string) {
prefix := fmt.Sprintf("(%s) %s ✦", timestamp, author)
prefix := log.Sprintf("(%s) %s ✦", timestamp, author)
// Print the prefix first, right-aligned.
fmt.Printf("%*s\n", termWidth, prefix)
log.Printf("%*s\n", termWidth, prefix)
// The available width for the text.
contentWidth := termWidth - 8 // Leave a tab's worth of margin on the left
@ -107,7 +107,7 @@ func printRightAligned(author, timestamp, content string) {
for _, paragraph := range paragraphs {
words := strings.Fields(paragraph)
if len(words) == 0 {
fmt.Println() // Preserve paragraph breaks
log.Println() // Preserve paragraph breaks
continue
}
@ -117,12 +117,12 @@ func printRightAligned(author, timestamp, content string) {
currentLine += " " + word
} else {
// Print the completed line, right-aligned.
fmt.Printf("%*s\n", termWidth, currentLine)
log.Printf("%*s\n", termWidth, currentLine)
currentLine = word
}
}
// Print the last remaining line of the paragraph, right-aligned.
fmt.Printf("%*s\n", termWidth, currentLine)
log.Printf("%*s\n", termWidth, currentLine)
}
}
@ -130,11 +130,11 @@ func printTable(table *chatpb.Table) {
if table == nil || len(table.GetRows()) == 0 {
return
}
fmt.Println("┌─[ Table Data ]──────────────────────────────────────────")
log.Println("┌─[ Table Data ]──────────────────────────────────────────")
for _, row := range table.GetRows() {
fmt.Printf("│ %s\n", strings.Join(row.GetFields(), " │ "))
log.Printf("│ %s\n", strings.Join(row.GetFields(), " │ "))
}
fmt.Printf("└─────────────────────────────────────────────────────────\n\n")
log.Printf("└─────────────────────────────────────────────────────────\n\n")
}
func printCodeSnippet(snippet *chatpb.CodeSnippet) {
@ -142,11 +142,11 @@ func printCodeSnippet(snippet *chatpb.CodeSnippet) {
code := snippet.GetContent()
language := filepath.Base(snippet.GetFilename()) // Still useful for display
fmt.Println() // Add extra line feed for spacing
log.Println() // Add extra line feed for spacing
// --- Top Border ---
topBorder := fmt.Sprintf("┌─[ Code Snippet: %s ]", language)
fmt.Printf("%s%s┐\n", topBorder, strings.Repeat("─", termWidth-len(topBorder)-1))
topBorder := log.Sprintf("┌─[ Code Snippet: %s ]", language)
log.Printf("%s%s┐\n", topBorder, strings.Repeat("─", termWidth-len(topBorder)-1))
// --- Content Lines ---
for _, line := range strings.Split(strings.TrimSpace(code), "\n") {
@ -155,17 +155,17 @@ func printCodeSnippet(snippet *chatpb.CodeSnippet) {
if padding < 0 {
padding = 0 // Should not happen with wrapping, but as a safeguard
}
fmt.Printf("│ %s%s │\n", line, strings.Repeat(" ", padding))
log.Printf("│ %s%s │\n", line, strings.Repeat(" ", padding))
}
// --- Bottom Border ---
fmt.Printf("└%s┘\n\n", strings.Repeat("─", termWidth-2))
log.Printf("└%s┘\n\n", strings.Repeat("─", termWidth-2))
}
func printToolCallBox(tc *chatpb.ToolCall) {
boxWidth := termWidth - 2
fmt.Printf(" ╭%s╮\n", strings.Repeat("─", boxWidth))
header := fmt.Sprintf(" ✔ %s %s (%s)", tc.GetName(), tc.GetInput(), tc.GetDescription())
log.Printf(" ╭%s╮\n", strings.Repeat("─", boxWidth))
header := log.Sprintf(" ✔ %s %s (%s)", tc.GetName(), tc.GetInput(), tc.GetDescription())
printWrappedLine(header, boxWidth)
printEmptyLine(boxWidth)
if stdout := tc.GetOutputStdout(); stdout != "" {
@ -179,7 +179,7 @@ func printToolCallBox(tc *chatpb.ToolCall) {
}
}
printEmptyLine(boxWidth)
fmt.Printf(" ╰%s╯\n", strings.Repeat("─", boxWidth))
log.Printf(" ╰%s╯\n", strings.Repeat("─", boxWidth))
}
func printWrappedLine(text string, width int) {
@ -188,12 +188,12 @@ func printWrappedLine(text string, width int) {
return
}
for len(text) > width {
fmt.Printf(" │ %-*s │\n", width, text[:width])
log.Printf(" │ %-*s │\n", width, text[:width])
text = text[width:]
}
fmt.Printf(" │ %-*s │\n", width, text)
log.Printf(" │ %-*s │\n", width, text)
}
func printEmptyLine(width int) {
fmt.Printf(" │ %*s │\n", width, "")
log.Printf(" │ %*s │\n", width, "")
}

69
stats.go Normal file
View File

@ -0,0 +1,69 @@
package main
/*
// The following are the Go equivalents of the gemini-cli's TypeScript
// interfaces for session statistics. You can use these to collect and
// store metrics in your application.
// ToolCallDecision represents the user's decision on a tool call.
type ToolCallDecision string
const (
Accept ToolCallDecision = "accept"
Reject ToolCallDecision = "reject"
Modify ToolCallDecision = "modify"
AutoAccept ToolCallDecision = "auto_accept"
)
// ToolCallStats holds the statistics for a single tool.
type ToolCallStats struct {
Count int
Success int
Fail int
DurationMs int
Decisions map[ToolCallDecision]int
}
// ModelMetrics holds the statistics for a single model.
type ModelMetrics struct {
API struct {
TotalRequests int
TotalErrors int
TotalLatencyMs int
}
Tokens struct {
Prompt int
Candidates int
Total int
Cached int
Thoughts int
Tool int
}
}
// SessionMetrics holds all the statistics for a session.
type SessionMetrics struct {
Models map[string]ModelMetrics
Tools struct {
TotalCalls int
TotalSuccess int
TotalFail int
TotalDurationMs int
TotalDecisions map[ToolCallDecision]int
ByName map[string]ToolCallStats
}
Files struct {
TotalLinesAdded int
TotalLinesRemoved int
}
}
// You will need to initialize and update this struct as your application
// makes API calls and runs tools.
var sessionMetrics SessionMetrics
// Example of how you might update the metrics after an API call:
// modelMetrics := sessionMetrics.Models["gemini-pro"]
// modelMetrics.API.TotalRequests++
// ... and so on.
*/

View File

@ -4,16 +4,24 @@
package main
import (
"context"
"go.wit.com/dev/alexflint/arg"
"go.wit.com/gui"
"go.wit.com/lib/gadgets"
"go.wit.com/lib/protobuf/chatpb"
"google.golang.org/genai"
)
var me *mainType
// this app's variables
type mainType struct {
pp *arg.Parser // for parsing the command line args. Yay to alexf lint!
chats *chatpb.Chats // all our prior conversations with regex
myGui *gui.Node // the gui toolkit handle
pp *arg.Parser // for parsing the command line args. Yay to alexf lint!
chats *chatpb.Chats // all our prior conversations with regex
client *genai.Client // the Google Gemini AI client variable
ctx context.Context // global context. what does this acutally mean?
lastChat *chatpb.Chat // the last chat. append to here
myGui *gui.Node // the gui toolkit handle
mainWindow *gadgets.GenericWindow // the main GUI window
}

27
submitChat.go Normal file
View File

@ -0,0 +1,27 @@
package main
import (
"fmt"
"go.wit.com/log"
"google.golang.org/genai"
)
func submitChat(content []*genai.Content) error {
resp, err := me.client.Models.GenerateContent(me.ctx, "gemini-2.5-flash", content, nil)
if err != nil {
return log.Errorf("error sending message: %v", err)
}
log.Info("Response from API:")
for _, cand := range resp.Candidates {
if cand.Content != nil {
for _, part := range cand.Content.Parts {
fmt.Println(part)
}
}
}
// TODO: add the response to the protobuf
addResponse(resp)
return nil
}

41
termSize.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
"log"
"os"
"golang.org/x/term"
)
// getTerminalWidth returns the width of the active terminal.
// If the output is not an interactive terminal (e.g., it's being piped to a file
// or another command), it returns a default width and false.
func getTerminalWidth() (int, bool) {
// term.IsTerminal checks if the given file descriptor is connected to a terminal.
// We use os.Stdout.Fd() to check the standard output.
if term.IsTerminal(int(os.Stdout.Fd())) {
// term.GetSize returns the dimensions of the given terminal.
width, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil {
// If we can't get the size for some reason, fall back to the default.
log.Printf("could not get terminal size: %v", err)
return 120, false
}
return width, true
}
// If it's not a terminal, return the default width.
return 120, false
}
// truncateString shortens a string to the specified length, adding an ellipsis if truncated.
func truncateString(s string, maxLength int) string {
if len(s) <= maxLength {
return s
}
// Subtract 3 to make room for the ellipsis "..."
if maxLength < 3 {
return "..."
}
return s[:maxLength-3] + "..."
}

View File

@ -1,16 +0,0 @@
package main
import (
"os"
"golang.org/x/term"
)
func getTerminalWidth() int {
width, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil {
// Return a default width if there's an error
return 80
}
return width
}

111
windowBook.go Normal file
View File

@ -0,0 +1,111 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
import (
"fmt"
"time"
"go.wit.com/lib/gadgets"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
)
func makeBookWindow(all *chatpb.Books) *gadgets.GenericWindow {
bookWin := gadgets.NewGenericWindow("regex Books", "Display Books")
bookWin.Win.Custom = func() {
log.Info("test delete window here")
}
grid := bookWin.Group.RawGrid()
var t *chatpb.BooksTable
grid.NewButton("show", func() {
if t != nil {
log.Info("Delete the table first")
return
}
// display the protobuf
t = addBooksPB(bookWin, all)
f := func(chat *chatpb.Book) {
log.Info("got to BookTable.Custom() id =", chat.GetUuid(), chat.GetTitle())
}
t.Custom(f)
log.Info("table has uuid", t.GetUuid())
})
grid.NewButton("delete PB table", func() {
if t != nil {
t.Delete()
t = nil
log.Info("Table should have been deleted")
}
})
// display the protobuf
t = addBooksPB(bookWin, all)
f := func(chat *chatpb.Book) {
log.Info("got to BookTable.Custom() id =", chat.GetUuid(), chat.GetTitle())
}
t.Custom(f)
log.Info("table has uuid", t.GetUuid())
return bookWin
}
func addBooksPB(win *gadgets.GenericWindow, pb *chatpb.Books) *chatpb.BooksTable {
t := pb.NewTable("testForgeRepos")
t.NewUuid()
tbox := win.Bottom.Box().SetProgName("TBOX")
t.SetParent(tbox)
sf := t.AddStringFunc("uuid", func(r *chatpb.Book) string {
return r.GetUuid()
})
sf.Custom = func(r *chatpb.Book) {
log.Info("todo: fix mouseClick() on stringFunc in GUI table", r.GetUuid())
}
// add a general show button
bf := t.AddButtonFunc("cur version", func(chat *chatpb.Book) string { return "submit" })
bf.Custom = func(book *chatpb.Book) {
log.Info("convert pb to genai GO API HERE", book.GetUuid())
if book.GetGeminiRequest() == nil {
return
}
aichat, err := convertToGenai(book.GetGeminiRequest())
if err != nil {
log.Info("convertToGenai() returned with error", err)
return
}
err = submitChat(aichat)
if err != nil {
log.Info("submitChat() returned with error", err)
return
}
log.Info("submitChat() appears to have worked?")
}
// show the age of the chat
t.AddTimeFunc("age", func(book *chatpb.Book) time.Time {
return book.GetCtime().AsTime()
})
t.AddTitle()
// t.AddVersion()
// make a button to show content in the *genai.GeminiRequest structures
genaiButton := t.AddButtonFunc("# of parts", func(book *chatpb.Book) string {
if book.GeminiRequest == nil {
return "nil"
}
var req = book.GeminiRequest
return fmt.Sprintf("%d", len(req.Contents))
})
genaiButton.Custom = func(book *chatpb.Book) {
log.Info("show *genai.GeminiRequsts for", book.GetUuid())
book.GeminiRequest.PrintGeminiTable()
}
// draw the tabel (send the gui protobuf to the GO plugin)
t.ShowTable()
return t
}

162
windowChats.go Normal file
View File

@ -0,0 +1,162 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package main
import (
"fmt"
"time"
"go.wit.com/lib/gadgets"
"go.wit.com/lib/protobuf/chatpb"
"go.wit.com/log"
)
/*
type stdReposTableWin struct {
sync.Mutex
win *gadgets.GenericWindow // the machines gui window
boxTB *gui.Node // the machines gui parent box widget
TB *gitpb.ReposTable // the gui table buffer
pb *gitpb.Repos // the current repos protobuf
update bool // if the window should be updated
}
func (w *stdReposTableWin) Toggle() {
if w == nil {
return
}
if w.win == nil {
return
}
w.win.Toggle()
}
*/
func makeChatsWindow() *gadgets.GenericWindow {
insertWin := gadgets.NewGenericWindow("regex Chats", "Display Chats")
insertWin.Win.Custom = func() {
log.Info("test delete window here")
insertWin.Hide()
}
grid := insertWin.Group.RawGrid()
var t *chatpb.ChatsTable
grid.NewButton("show", func() {
if t != nil {
log.Info("Delete the table first")
return
}
// display the protobuf
t = addChatsPB(insertWin, me.chats)
f := func(chat *chatpb.Chat) {
log.Info("got to ChatTable.Custom() id =", chat.GetUuid(), chat.GetChatName())
}
t.Custom(f)
log.Info("table has uuid", t.GetUuid())
})
grid.NewButton("delete PB table", func() {
if t != nil {
t.Delete()
t = nil
log.Info("Table should have been deleted")
}
})
// display the protobuf
t = addChatsPB(insertWin, me.chats)
f := func(chat *chatpb.Chat) {
log.Info("got to ChatTable.Custom() id =", chat.GetUuid(), chat.GetChatName())
}
t.Custom(f)
log.Info("table has uuid", t.GetUuid())
return insertWin
}
func addChatsPB(win *gadgets.GenericWindow, pb *chatpb.Chats) *chatpb.ChatsTable {
t := pb.NewTable("testForgeRepos")
t.NewUuid()
tbox := win.Bottom.Box().SetProgName("TBOX")
t.SetParent(tbox)
sf := t.AddStringFunc("uuid", func(r *chatpb.Chat) string {
return r.GetUuid()
})
sf.Custom = func(r *chatpb.Chat) {
log.Info("todo: fix mouseClick() on stringFunc in GUI table", r.GetUuid())
}
// add a general show button
bf := t.AddButtonFunc("cur version", func(chat *chatpb.Chat) string { return "show" })
bf.Custom = func(r *chatpb.Chat) {
log.Info("todo: show a chat window here", r.GetUuid())
}
// show the age of the chat
t.AddTimeFunc("age", func(chat *chatpb.Chat) time.Time {
return chat.GetCtime().AsTime()
})
t.AddChatName()
// make a button to show the ChatEntries
entryButton := t.AddButtonFunc("Entries", func(chat *chatpb.Chat) string {
return fmt.Sprintf("%d", len(chat.GetEntries()))
})
entryButton.Custom = func(chat *chatpb.Chat) {
log.Info("show entries for", chat.GetUuid())
chat.PrintChatEntriesTable()
}
// make a button to show the Stats (old stuff from gemini-cli)
statsButton := t.AddButtonFunc("Stats", func(chat *chatpb.Chat) string {
return fmt.Sprintf("%d", len(chat.GetSession()))
})
statsButton.Custom = func(chat *chatpb.Chat) {
log.Info("show gemini-cli /stats for", chat.GetUuid())
chat.PrintChatStatsTable()
}
// make a button to show content in the *genai.GeminiRequest structures
genaiButton := t.AddButtonFunc("# of genai.Req's", func(chat *chatpb.Chat) string {
var counter int
for _, entry := range chat.GetEntries() {
if entry.GeminiRequest != nil {
counter += 1
}
}
return fmt.Sprintf("%d", counter)
})
genaiButton.Custom = func(chat *chatpb.Chat) {
log.Info("show *genai.GeminiRequsts for", chat.GetUuid())
chat.PrintChatGeminiTable()
pb := makeBooksTable(chat)
makeBookWindow(pb)
}
// draw the tabel (send the gui protobuf to the GO plugin)
t.ShowTable()
return t
}
func makeBooksTable(chat *chatpb.Chat) *chatpb.Books {
var pb *chatpb.Books
pb = chatpb.NewBooks()
pb.TitleUuid = chat.Uuid
for _, entry := range chat.GetEntries() {
if entry.GeminiRequest == nil {
continue
}
newb := new(chatpb.Book)
newb.Ctime = entry.Ctime
newb.Uuid = entry.Uuid
newb.From = entry.From
newb.Title = entry.ContentFile
newb.GeminiRequest = entry.GeminiRequest
pb.Append(newb)
}
pb.ConfigSave()
return pb
}